Selaa lähdekoodia

fix(python-sdk): harden profile runtime startup

Resolve packaged profile proxies with Node ESM import conditions from each package installation, and fail loud when an explicit runtime export or legacy main entry is missing. Serialize the shared profile fallback under the existing cross-process writer lock so concurrent dsh processes cannot observe partial proxies; either carrier now replaces the other carrier’s managed entry without manual cleanup.

Give Python initialize its own 10-second default bound and name the selected profile in timeout diagnostics, while leaving ordinary agent turns unbounded by default. Package the dynamically resolved web frontend and skill-badge assets so the runtime wheel’s normal dsh profiles do not depend on pkg static-discovery accidents.

Rewrite the root launch rule and every active stale SDK-runtime note to the shipped dsh profile architecture in both languages. Focused tests prove import-only and transitive package exports, lock contention, cross-carrier transitions, missing-entry failures, asset inventory, and bounded initialization.
Tianyi Cui 1 kuukausi sitten
vanhempi
sitoutus
9edf1b9f10
35 muutettua tiedostoa jossa 311 lisäystä ja 117 poistoa
  1. 2 2
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.i18n.yaml
  2. 1 1
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md
  3. 1 1
      .agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md
  4. 2 2
      .agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.i18n.yaml
  5. 7 11
      .agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md
  6. 7 11
      .agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md
  7. 2 2
      .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.i18n.yaml
  8. 3 3
      .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md
  9. 3 3
      .agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md
  10. 2 2
      .agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.i18n.yaml
  11. 1 1
      .agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md
  12. 1 1
      .agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md
  13. 1 1
      AGENTS.md
  14. 1 0
      THIRD_PARTY_NOTICES.md
  15. 1 1
      apps/cli/src/bin.ts
  16. 3 2
      apps/cli/src/dump-config.ts
  17. 6 6
      apps/cli/src/profile-boot.ts
  18. 1 1
      apps/cli/tests/web-agent-presets.e2e.ts
  19. 1 1
      apps/web/tests/scaffold.ts
  20. 2 2
      packages/boot/app-boot/README.i18n.yaml
  21. 0 0
      packages/boot/app-boot/README.md
  22. 0 0
      packages/boot/app-boot/README.zh.md
  23. 2 0
      packages/boot/app-boot/package.json
  24. 63 29
      packages/boot/app-boot/src/profile.ts
  25. 136 28
      packages/boot/app-boot/tests/profile.spec.ts
  26. 3 0
      packages/boot/app-boot/tsconfig.json
  27. 6 0
      pnpm-lock.yaml
  28. 2 2
      python/sdk/README.i18n.yaml
  29. 1 1
      python/sdk/README.md
  30. 1 1
      python/sdk/README.zh.md
  31. 2 0
      python/sdk/src/deepseek_harness/api.py
  32. 10 1
      python/sdk/src/deepseek_harness/client.py
  33. 5 1
      python/sdk/tests/test_client.py
  34. 27 0
      scripts/build-exe-for-python-sdk-assets.spec.ts
  35. 5 0
      scripts/build-exe-for-python-sdk.ts

+ 2 - 2
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.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-07-10-single-file-executable-sdk-runtime-distribution.md
-2026-07-10-single-file-executable-sdk-runtime-distribution.md: 50e63bbc4f664f9450f01acf45888991a27517d2
-2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: cf92f42f508f005c64f09aa0127b28ed162cede5
+2026-07-10-single-file-executable-sdk-runtime-distribution.md: e46dbbc119e2078e44632d81b333c8be5ab9d6d7
+2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md: d7a1e3f445ea1c1f03df2b349a4391534a5502c3

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1 - 1
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.md


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 1 - 1
.agents/notes/implemented/architecture/2026-07-10-single-file-executable-sdk-runtime-distribution.zh.md


+ 2 - 2
.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.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-22-single-dsh-application-launcher.md
-2026-08-22-single-dsh-application-launcher.md: 69188e806d9192d230d1f1b52daf27a3b30481be
-2026-08-22-single-dsh-application-launcher.zh.md: a6bb1909b019ea0d386bd2f4a1bb8f5199ef1974
+2026-08-22-single-dsh-application-launcher.md: 27ec766dc62a203a3054ec67828b8897ab5ad245
+2026-08-22-single-dsh-application-launcher.zh.md: 5fb7fb4234b093a6f69446e0d9b1d7fbb7f46cf3

+ 7 - 11
.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.md

@@ -8,7 +8,7 @@ English | [中文](2026-08-22-single-dsh-application-launcher.zh.md)
 
 DeepSeek Harness application processes need one owner for composition, plugin resolution, environment discovery, shutdown, and user customization. A dedicated app bin with a complete `cordis.yml` creates a second lifecycle beside profile launch: plugins installed into a profile do not reach it, behavior drifts from `dsh-base`, and SDK callers learn arbitrary process argv instead of the product's composition model.
 
-The Python SDK distributes a native executable and three platform wheels whose embedded direct-config runtime cannot change launch architecture without rebuilding and validating the complete VFS closure. That distribution needs an explicit temporary exception, not a second general Node application pattern.
+The Python SDK distributes a native executable and three platform wheels. Its packaged process must use the same profile launcher while preserving the closed VFS dependency tree, native sidecars, and installed-wheel evidence.
 
 ## Decision
 
@@ -43,19 +43,15 @@ SDK users customize plugins through profiles. `dsh plugin --profile <name> ...`
 
 Direct SDK use follows normal Harness-home resolution: explicit `dshHome`, inherited `DSH_HOME`, then `~/.dsh`. `subagent-dsh-sdk` instead requires an explicit absolute home, so a nested runtime cannot discover a person's profiles, installed plugins, credentials, or sessions through the operating-system home. DSH-specific ACP child examples also pass an isolated home; the ACP backend itself remains generic for non-DSH agents.
 
-### Python exception and names
+### Python runtime
 
-The Python SDK's direct-config application lives in the private `packages/sdk/python-runtime` package named `@deepseek-ai/dsh-sdk-python-runtime`. Its only packaged executable entry is `lib/packaged-bin.js`, consumed by the private `dsh-sdk-python-runtime-closure` deploy root. It has no public npm bin. The runnable direct Python example is `examples/python-sdk-agent`.
+The Python runtime wheel packages the ordinary `@deepseek-ai/dsh` CLI from `node_modules/@deepseek-ai/dsh/lib/bin.js` through the private `dsh-python-runtime-closure` deploy manifest. The Python client selects `dsh --profile sdk`, ordered patch files, and an explicit Harness home; the installed `dsh` console command exposes the same profile grammar. The runnable Python example is `examples/python-sdk-agent`.
 
-Python-observable behavior remains fixed: Python API, SDK wire, default `cordis.yml`, environment variables, wheel distribution names, packaged executable names, sidecar names, explicit runtime options, zero-config behavior, and supported platforms. The stable SDK family remains `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, `@deepseek-ai/dsh-sdk-jsonrpc-server`, and wire identity `deepseek-harness-sdk-runtime`; `@deepseek-ai/dsh-acp` remains the ACP protocol plugin. There is no compatibility package, forwarding executable, fallback parser, or SDK/ACP launcher alias.
+The executable family is `deepseek-harness-sdk-runtime-<platform>-<arch>`. The SDK wire, wheel and import distribution names, sidecar names, and wire identity `deepseek-harness-sdk-runtime` remain stable. The SDK package family is `@deepseek-ai/dsh-sdk-client`, `@deepseek-ai/dsh-sdk-protocol`, and `@deepseek-ai/dsh-sdk-jsonrpc-server`; `@deepseek-ai/dsh-acp` remains the ACP protocol plugin. There is no Python-specific Node application, checked-in complete config, compatibility package, forwarding executable, fallback parser, or SDK/ACP launcher alias.
 
 ### Enforcement
 
-`verify-application-entrypoints` scans application/package manifests, executable sources, and root demo scripts. The allowlist classifies the `dsh` product bin, vendor-excluded scope, the private WebWorker build tool, test support, and the private Python carrier. An unclassified shebang, a new package bin, or a demo wrapper that bypasses `apps/cli/src/bin.ts` fails hygiene and the primary/static CI aggregates.
-
-## Deferred Python migration
-
-The Python runtime follow-up must move the packaged process through `dsh --profile sdk`, preserve the wheel's closed dependency and native sidecar behavior, and delete `@deepseek-ai/dsh-sdk-python-runtime`. Only after those conditions pass on Linux x64, Linux arm64, and macOS arm64 does the executable family change from `dsh-jsonrpc-agent-pkg-<platform>-<arch>` to `deepseek-harness-sdk-runtime-<platform>-<arch>`. The temporary carrier and current artifact names make that obligation visible without weakening current Python compatibility.
+`verify-application-entrypoints` scans application/package manifests, executable sources, and root demo scripts. The allowlist classifies the `dsh` product bin, vendor-excluded scope, the private WebWorker build tool, and test support. An unclassified shebang, a new package bin, or a demo wrapper that bypasses `apps/cli/src/bin.ts` fails hygiene and the primary/static CI aggregates.
 
 ## Existing decisions and supersession
 
@@ -88,7 +84,7 @@ The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-o
 - Focused unit suites cover profile launch resolution, initialization bounds, SDK retries, server readiness, and nested isolated homes with 100% coverage on the changed runtime sources.
 - Keyless ACP and SDK snapshots boot real `dsh` profiles and pin protocol output plus persisted logs; the nested SDK composition boots a second real profile runtime.
 - The real-API workflow caps file parallelism at four because one profile e2e file can own several complete `dsh` subprocess trees; workflow tests pin that resource bound.
-- The Python suite exercises exe and node carriers; all packaged-runtime scenarios, native macOS executable construction, both wheels, and clean-wheel default/MCP smokes retain the existing artifact names.
+- The Python suite exercises exe and node carriers; packaged-runtime scenarios, native macOS executable construction, both wheels, and clean-wheel default/MCP smokes pin the `deepseek-harness-sdk-runtime-*` artifacts and profile launch.
 - `verify-application-entrypoints` includes invalid fixtures for package bins, executable sources, package-launching demo wrappers, and unclassified demos.
 
 ## Consequences
@@ -98,4 +94,4 @@ The [ACP automation-only protocol](../simplification/2026-07-23-acp-automation-o
 - SDK and ACP share the complete base application and one set of policy and tools; snapshots present intentional assembled differences explicitly.
 - Adding `@deepseek-ai/dsh` increases the TypeScript client's install size in exchange for a deterministic same-version runtime.
 - Trusted user patches can add a plugin that writes to stdout and corrupt their own protocol stream; shipped profiles guarantee purity, not arbitrary third-party composition.
-- Python keeps a visibly private, narrowly allowed direct-config carrier until its platform artifact migration is independently proven.
+- Python packages the ordinary `dsh` profile launcher while retaining a closed native runtime and no system-Node requirement for wheel users.

+ 7 - 11
.agents/notes/implemented/architecture/2026-08-22-single-dsh-application-launcher.zh.md

@@ -8,7 +8,7 @@ Status: implemented
 
 DeepSeek Harness 应用进程需要由同一个机制负责组合、插件解析、环境发现、关闭和用户自定义。带完整 `cordis.yml` 的专用应用 bin 会在 profile 启动之外形成第二套生命周期:安装到 profile 的插件无法到达它,行为会与 `dsh-base` 偏离,SDK 调用方还需要学习任意进程 argv,而不是产品的组合模型。
 
-Python SDK 分发一个原生可执行文件和三个平台 wheel 包;其中嵌入的直读配置运行时只有在重建并验证完整 VFS 闭包后才能改变启动架构。该分发需要一个明确的临时例外,而不是另一种通用 Node 应用模式
+Python SDK 分发一个原生可执行文件和三个平台 wheel 包。其打包进程必须使用同一 profile 启动器,同时保留封闭的 VFS 依赖树、原生伴随文件与 installed-wheel 证据
 
 ## Decision
 
@@ -43,19 +43,15 @@ SDK 用户通过 profile 自定义插件。`dsh plugin --profile <name> ...` 管
 
 直接使用 SDK 时遵循普通 Harness home 解析:显式 `dshHome`、继承的 `DSH_HOME`,最后是 `~/.dsh`。`subagent-dsh-sdk` 则要求显式绝对 home,因此嵌套运行时不会通过操作系统 home 发现个人 profile、已安装插件、凭据或会话。DSH 专用 ACP 子进程示例同样传入隔离 home;ACP 后端自身继续适用于非 DSH agent。
 
-### Python 例外与命名
+### Python 运行时
 
-Python SDK 的直读配置应用位于私有 `packages/sdk/python-runtime` 包,名称是 `@deepseek-ai/dsh-sdk-python-runtime`。它唯一的打包可执行入口是 `lib/packaged-bin.js`,由私有 `dsh-sdk-python-runtime-closure` 部署根消费。它没有公开 npm bin。可运行的直启 Python 示例是 `examples/python-sdk-agent`。
+Python 运行时 wheel 通过私有 `dsh-python-runtime-closure` 部署 manifest,打包来自 `node_modules/@deepseek-ai/dsh/lib/bin.js` 的普通 `@deepseek-ai/dsh` CLI。Python 客户端选择 `dsh --profile sdk`、有序 patch 文件和显式 Harness home;安装的 `dsh` 控制台命令暴露相同 profile 语法。可运行的 Python 示例是 `examples/python-sdk-agent`。
 
-Python 可观察行为保持不变:Python API、SDK 协议格式、默认 `cordis.yml`、环境变量、wheel 包分发名称、打包可执行文件名称、伴随文件名称、显式运行时选项、零配置行为与支持平台。稳定 SDK 包族继续是 `@deepseek-ai/dsh-sdk-client`、`@deepseek-ai/dsh-sdk-protocol`、`@deepseek-ai/dsh-sdk-jsonrpc-server`,协议 identity 继续是 `deepseek-harness-sdk-runtime`;`@deepseek-ai/dsh-acp` 继续作为 ACP 协议插件。仓库不保留兼容包、转发可执行文件、后备解析器或 SDK/ACP 启动别名。
+可执行文件族是 `deepseek-harness-sdk-runtime-<platform>-<arch>`。SDK 协议格式、wheel 与 import 分发名称、伴随文件名称,以及协议 identity `deepseek-harness-sdk-runtime` 保持稳定。SDK 包族是 `@deepseek-ai/dsh-sdk-client`、`@deepseek-ai/dsh-sdk-protocol` 与 `@deepseek-ai/dsh-sdk-jsonrpc-server`;`@deepseek-ai/dsh-acp` 继续作为 ACP 协议插件。仓库不保留 Python 专用 Node 应用、检入的完整配置、兼容包、转发可执行文件、后备解析器或 SDK/ACP 启动别名。
 
 ### 强制校验
 
-`verify-application-entrypoints` 扫描应用/包 manifest、可执行源码和根 demo 脚本。允许清单对 `dsh` 产品 bin、排除的 vendor 范围、私有 WebWorker 构建工具、测试支持以及私有 Python 载体进行分类。未分类的 shebang、新包 bin 或绕过 `apps/cli/src/bin.ts` 的 demo wrapper 都会使 hygiene 与 primary/static CI 聚合失败。
-
-## 暂缓的 Python 迁移
-
-Python 运行时后续工作必须把打包进程迁移到 `dsh --profile sdk`,保持 wheel 包的封闭依赖与原生伴随文件行为,并删除 `@deepseek-ai/dsh-sdk-python-runtime`。只有这些条件在 Linux x64、Linux arm64 与 macOS arm64 全部通过后,可执行文件族才会从 `dsh-jsonrpc-agent-pkg-<platform>-<arch>` 改名为 `deepseek-harness-sdk-runtime-<platform>-<arch>`。临时载体与当前产物名称使这项义务清晰可见,同时不削弱当前 Python 兼容性。
+`verify-application-entrypoints` 扫描应用/包 manifest、可执行源码和根 demo 脚本。允许清单对 `dsh` 产品 bin、排除的 vendor 范围、私有 WebWorker 构建工具和测试支持进行分类。未分类的 shebang、新包 bin 或绕过 `apps/cli/src/bin.ts` 的 demo wrapper 都会使 hygiene 与 primary/static CI 聚合失败。
 
 ## 既有决策与取代关系
 
@@ -88,7 +84,7 @@ Python 运行时后续工作必须把打包进程迁移到 `dsh --profile sdk`
 - 聚焦单元套件覆盖 profile 启动解析、初始化时限、SDK 重试、服务器就绪和嵌套隔离 home,并对变更后的运行时源码实现 100% 覆盖率。
 - 免密钥 ACP 与 SDK 快照启动真实 `dsh` profile,并钉住协议输出与持久化日志;嵌套 SDK 组合会启动第二个真实 profile 运行时。
 - 真实 API 工作流把文件并行度限制为 4,因为一个 profile e2e 文件可能拥有多个完整 `dsh` 子进程树;工作流测试会钉住该资源上限。
-- Python 套件同时测试 exe 与 node 载体;全部打包运行时场景、原生 macOS 可执行文件构建、两个 wheel 包以及干净 wheel 默认/MCP 冒烟测试都保留既有产物名称
+- Python 套件同时测试 exe 与 node 载体;打包运行时场景、原生 macOS 可执行文件构建、两个 wheel 包以及干净 wheel 默认/MCP 冒烟测试会钉住 `deepseek-harness-sdk-runtime-*` 产物与 profile 启动
 - `verify-application-entrypoints` 包含包 bin、可执行源码、直启包的 demo wrapper 与未分类 demo 等非法 fixture(测试前置数据)。
 
 ## 影响
@@ -98,4 +94,4 @@ Python 运行时后续工作必须把打包进程迁移到 `dsh --profile sdk`
 - SDK 与 ACP 共享完整 base 应用和同一份策略与工具;快照以显式差异呈现刻意采用的组装变化。
 - 增加 `@deepseek-ai/dsh` 会扩大 TypeScript 客户端的安装体积,换来确定的同版本运行时。
 - 受信任用户 patch 可以增加写入 stdout 的插件并破坏自己的协议流;随附 profile 保证纯净,不为任意第三方组合提供保证。
-- Python 保留一个清晰可见的私有直读配置载体,直到其平台产物迁移得到独立证明
+- Python 打包普通 `dsh` profile 启动器,同时保留封闭原生运行时,wheel 用户无需系统 Node

+ 2 - 2
.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.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-23-python-sdk-dsh-profile-runtime.md
-2026-08-23-python-sdk-dsh-profile-runtime.md: 400915155460598e754c2f81d60b26e693ff438a
-2026-08-23-python-sdk-dsh-profile-runtime.zh.md: a3c9074817d1c1e9b3f59fbe5fb02910d20e02f4
+2026-08-23-python-sdk-dsh-profile-runtime.md: 14c58c85923aac1b54362c72f61f7f1e1c145124
+2026-08-23-python-sdk-dsh-profile-runtime.zh.md: 9f584ca9bfcc7907caede70ef7e9781f2bc37aab

+ 3 - 3
.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.md

@@ -16,13 +16,13 @@ A normal profile cannot be adopted only at the Python wrapper. The runtime execu
 
 The runtime executable packages `@deepseek-ai/dsh` and runs its ordinary command grammar. The Python client selects `--profile sdk` by default, forwards ordered absolute `--patch` paths, and may select another `dsh` executable or profile. The private `@deepseek-ai/dsh-sdk-python-runtime` application package and checked-in runtime `cordis.yml` do not exist. JSON-RPC serving remains the `@deepseek-ai/dsh-sdk-app` bundle and `@deepseek-ai/dsh-sdk-jsonrpc-server` plugin, not a Python-owned boot path.
 
-The public Python configuration is `dsh_bin`, `profile`, ordered `patches`, `dsh_home`, process cwd/environment, provider/model/token selection, and timeouts. It does not expose a complete Cordis tree or arbitrary launch argv. `RunResult` reports the protocol-owned run values and does not duplicate the profile's persistence path.
+The public Python configuration is `dsh_bin`, `profile`, ordered `patches`, `dsh_home`, process cwd/environment, provider/model/token selection, a bounded initialization timeout, and optional turn/shutdown timeouts. It does not expose a complete Cordis tree or arbitrary launch argv. `RunResult` reports the protocol-owned run values and does not duplicate the profile's persistence path.
 
 Every Python launch requires either explicit `dsh_home` or a non-empty `DSH_HOME` in the child environment. The SDK never discovers `~/.dsh`. The selected home consistently owns profiles, external plugins, credentials, settings, and sessions.
 
 ### Plugin customization
 
-Persistent SDK customization uses the same profile interfaces as direct CLI use. `dsh plugin --profile sdk ...` manages external dependencies and bundle order, `$DSH_HOME/profiles/sdk/cordis.patch.yml` owns persistent row changes, the home patch applies machine-local changes across profiles, and Python `patches` supplies invocation-specific overlays. A different profile is valid only when it retains an SDK server row. Missing profiles, bundles, server rows, and invalid patches fail without a complete-config fallback.
+Persistent SDK customization uses the same profile interfaces as direct CLI use. `dsh plugin --profile sdk ...` manages external dependencies and bundle order, `$DSH_HOME/profiles/sdk/cordis.patch.yml` owns persistent row changes, the home patch applies machine-local changes across profiles, and Python `patches` supplies invocation-specific overlays. A different profile is valid only when it retains an SDK server row. Missing profiles, bundles, server rows, and invalid patches fail without a complete-config fallback; a profile that remains alive without serving JSON-RPC fails the independently bounded initialization handshake with a diagnostic naming that profile.
 
 The runtime wheel installs a `dsh` console command. Ordinary profile and SDK execution remains Node-free; external package management requires a caller-installed `pnpm`.
 
@@ -30,7 +30,7 @@ The runtime wheel installs a `dsh` console command. Ordinary profile and SDK exe
 
 The zero-code deployment manifest is `dsh-python-runtime-closure`. It packages `node_modules/@deepseek-ai/dsh/lib/bin.js` and profile, bundle, preset, native-addon, and shared-library assets into `deepseek-harness-sdk-runtime-<platform>-<arch>`. The wheel distribution names, Python import modules, JSON-RPC messages, and wire-stable `serverInfo.name = deepseek-harness-sdk-runtime` remain unchanged.
 
-Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy mirrors the source package's explicit runtime exports and identity and re-exports its virtual module URLs. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module.
+Plain Node profiles use symlinks in `$DSH_HOME/profiles/node_modules` to share installation packages with external plugins. An operating-system symlink cannot traverse pkg's `/snapshot` filesystem, so the packaged CLI writes small real ESM proxy packages instead. Each proxy resolves the source package's explicit runtime exports under ESM import conditions and re-exports its virtual module URLs. One cross-process writer lock serializes fallback healing, preventing partial proxy visibility and allowing either carrier to replace the other carrier's managed entry. Loader rows and external plugin peers therefore resolve through the normal profile parent walk while retaining one Cordis and one instance of each bundled module.
 
 The published target set is Linux x64, Linux arm64, and macOS arm64. Installed-wheel black-box CI owns artifact provenance, default and patched profiles, external bundle installation, native tools, MCP, direct JSON-RPC, snapshots, and trusted real-provider turns on every target.
 

+ 3 - 3
.agents/notes/implemented/architecture/2026-08-23-python-sdk-dsh-profile-runtime.zh.md

@@ -16,13 +16,13 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml
 
 运行时可执行程序打包 `@deepseek-ai/dsh` 并运行其普通命令语法。Python 客户端默认选择 `--profile sdk`,转发有序绝对 `--patch` 路径,也可以选择另一个 `dsh` 可执行程序或 profile。私有 `@deepseek-ai/dsh-sdk-python-runtime` 应用包和检入的运行时 `cordis.yml` 均不存在。JSON-RPC 服务仍由 `@deepseek-ai/dsh-sdk-app` bundle 与 `@deepseek-ai/dsh-sdk-jsonrpc-server` 插件提供,而不是 Python 自有启动路径。
 
-公开 Python 配置包括 `dsh_bin`、`profile`、有序 `patches`、`dsh_home`、进程 cwd/环境、provider/model/token 选择 timeout。它不暴露完整 Cordis 树或任意启动 argv。`RunResult` 报告协议所有的运行值,不重复 profile 的持久化路径。
+公开 Python 配置包括 `dsh_bin`、`profile`、有序 `patches`、`dsh_home`、进程 cwd/环境、provider/model/token 选择、有界初始化 timeout,以及可选的轮次/关闭 timeout。它不暴露完整 Cordis 树或任意启动 argv。`RunResult` 报告协议所有的运行值,不重复 profile 的持久化路径。
 
 每次 Python 启动都要求显式 `dsh_home`,或子进程环境中的非空 `DSH_HOME`。SDK 绝不会发现 `~/.dsh`。所选 home 统一拥有 profile、外部插件、凭据、设置与会话。
 
 ### 插件自定义
 
-持久 SDK 自定义使用与直接 CLI 相同的 profile 接口。`dsh plugin --profile sdk ...` 管理外部依赖与 bundle 顺序,`$DSH_HOME/profiles/sdk/cordis.patch.yml` 负责持久配置项变更,home patch 对所有 profile 应用机器本地变更,Python `patches` 则提供单次启动 overlay。另一个 profile 只有保留 SDK server 配置项时才有效。缺失 profile、bundle、server 配置项或非法 patch 都会直接失败,不存在完整配置回退。
+持久 SDK 自定义使用与直接 CLI 相同的 profile 接口。`dsh plugin --profile sdk ...` 管理外部依赖与 bundle 顺序,`$DSH_HOME/profiles/sdk/cordis.patch.yml` 负责持久配置项变更,home patch 对所有 profile 应用机器本地变更,Python `patches` 则提供单次启动 overlay。另一个 profile 只有保留 SDK server 配置项时才有效。缺失 profile、bundle、server 配置项或非法 patch 都会直接失败,不存在完整配置回退;保持运行却不提供 JSON-RPC 服务的 profile 会在独立有界的初始化握手中失败,诊断会指明该 profile
 
 运行时 wheel 安装 `dsh` 控制台命令。普通 profile 与 SDK 运行仍不需要 Node;外部包管理要求调用方自行安装 `pnpm`。
 
@@ -30,7 +30,7 @@ Python SDK 分发一个私有 Node 应用,直接启动完整外部 `cordis.yml
 
 零代码部署 manifest 是 `dsh-python-runtime-closure`。它把 `node_modules/@deepseek-ai/dsh/lib/bin.js` 以及 profile、bundle、preset、原生 addon 与共享库资源打包进 `deepseek-harness-sdk-runtime-<platform>-<arch>`。Wheel distribution 名称、Python import 模块、JSON-RPC 消息和协议稳定的 `serverInfo.name = deepseek-harness-sdk-runtime` 保持不变。
 
-普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理镜像源包的显式运行时 exports 与身份,并重新导出其虚拟模块 URL。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。
+普通 Node profile 在 `$DSH_HOME/profiles/node_modules` 中使用符号链接,让外部插件共享安装包。操作系统符号链接无法进入 pkg 的 `/snapshot` 文件系统,因此打包 CLI 改为写入小型真实 ESM 代理包。每个代理按 ESM import 条件解析源包的显式运行时 exports,并重新导出其虚拟模块 URL。一把跨进程写入锁会串行执行后备修复,避免暴露未完整写入的代理,并允许任一载体替换另一载体留下的受管条目。Loader 配置项和外部插件 peer 因而可以通过普通 profile 逐级向上查找解析,同时保留一个 Cordis 和每个内置模块的单一实例。
 
 已发布目标集合是 Linux x64、Linux arm64 与 macOS arm64。Installed-wheel 黑盒 CI 在每个目标上负责产物来源、默认及 patched profile、外部 bundle 安装、原生工具、MCP、直接 JSON-RPC、快照,以及可信真实提供方轮次。
 

+ 2 - 2
.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.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/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md
-2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: eda9d3a3de91944a298070d6cc22f632294f7a28
-2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: 1a72c6b1cdb7453b8468d6e6be37aec76323f714
+2026-07-27-typescript-sdk-and-sdk-subagent-backend.md: 37c341e964b556c7ab5fdd9081416883066b97d1
+2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md: e97e951028de3bcda9fe11be0351072481c72dd9

+ 1 - 1
.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.md

@@ -17,7 +17,7 @@ Three packages, layered exactly like the existing Python stack, plus one Service
 - **`@deepseek-ai/dsh-subagent-dsh-sdk`** (`packages/subagent/subagent-dsh-sdk/`) — the second out-of-process `SubagentProvider`, structured as `subagent-acp`'s sibling: same all-false capabilities and `inheritsParentContext: false`, same publish-after-handshake ownership transaction, same result-never-rejects flattening through an `onError` sink, same parent-namespace run id. The child answer is read from streamed `session.event`s — the last complete `assistant/message`, else accumulated `text-delta` chunks, so partial answers survive cancellation. Stop reasons map from the child's structured `TurnEndReason` (`completed`/`max-tokens`/`aborted` pass through; everything else, including a settled-without-turn child, is `error`). Its `dshBin`/profile/patch/home config selects an isolated SDK application, `provider`/`model` feeds the child's `initialize`, and `env` supplies explicit child-only values such as its API key.
 - **The subagent seam grows `out-of-process.ts`**: the provider-side vocabulary both out-of-process backends share — `NO_START_CAPABILITIES`, timing-bound validation, child cwd resolution (config override, else the delegating parent session's workspace), the never-reject `settleRunResult`, and the `subprocessRunHandle` publication. Process mechanics (spawn, env scrub, tree-scoped teardown) live in the `dsh-subprocess` seam; `subagent-acp` spawns through `ctx.subprocess`, while this backend spawns through the SDK client (the subprocess README's documented exception for SDK-managed transports) and applies the seam's `scrubbedParentEnv()` itself.
 
-`dsh-sdk-jsonrpc-server` keeps serving unchanged (the wire is byte-identical); the private `@deepseek-ai/dsh-sdk-python-runtime` carrier consumes the shared protocol through its packaged closure.
+`dsh-sdk-jsonrpc-server` keeps serving unchanged (the wire is byte-identical). TypeScript and Python clients both consume the shared protocol through `dsh --profile sdk`; the Python wheel packages that CLI and its closed dependency tree.
 
 ## Testing
 

+ 1 - 1
.agents/notes/implemented/feature/2026-07-27-typescript-sdk-and-sdk-subagent-backend.zh.md

@@ -17,7 +17,7 @@ stdio JSON-RPC 对外服务接口(`@deepseek-ai/dsh-sdk-jsonrpc-server`,见[
 - **`@deepseek-ai/dsh-subagent-dsh-sdk`**(`packages/subagent/subagent-dsh-sdk/`)—— 第二个进程外 `SubagentProvider`,采用与 `subagent-acp` 对等的结构:同样的全 false 能力与 `inheritsParentContext: false`,同样的握手后发布所有权事务,同样通过 `onError` sink 将结果归一为绝不拒绝,同样的父命名空间 run id。子答案从流式 `session.event` 读取——最后一条完整 `assistant/message`,否则累积的 `text-delta` 块,部分答案在取消时得以保留。停止原因由子进程的结构化 `TurnEndReason` 映射(`completed`/`max-tokens`/`aborted` 直通;其余一切、包括未运行任何轮次便已结束的子进程,都是 `error`)。其 `dshBin`/profile/patch/home 配置选择隔离的 SDK 应用,`provider`/`model` 写入子进程 `initialize`,`env` 则提供子进程专用的显式值,例如其 API key。
 - **subagent seam 新增 `out-of-process.ts`**:两个进程外后端共享的 provider 侧词汇——`NO_START_CAPABILITIES`、时限校验、子进程 cwd 解析(配置覆盖、否则发起委托的父会话工作区)、绝不拒绝的 `settleRunResult`、以及 `subprocessRunHandle` 发布。进程机制(spawn、环境清理、进程树清理)属于 `dsh-subprocess` seam;`subagent-acp` 经 `ctx.subprocess` spawn 子进程,本后端则经 SDK 客户端 spawn 子进程(subprocess README 记载的 SDK 托管传输例外)并自行应用该 seam 的 `scrubbedParentEnv()`。
 
-`dsh-sdk-jsonrpc-server` 的服务不变(协议字节完全一致);私有 `@deepseek-ai/dsh-sdk-python-runtime` 载体通过其打包闭包消费共享协议
+`dsh-sdk-jsonrpc-server` 的服务不变(协议字节完全一致)。TypeScript 与 Python 客户端都通过 `dsh --profile sdk` 消费共享协议;Python wheel 会打包该 CLI 及其封闭依赖树
 
 ## 测试
 

+ 1 - 1
AGENTS.md

@@ -8,7 +8,7 @@ DeepSeek Harness is an all-plugin agent harness on vendored Cordis. Read [docs/a
 
 ## Application launch
 
-Node apps launch only through `dsh` profiles; application-package bins, demos, and SDK argv escape hatches are forbidden. The private Python runtime is the sole temporary exception. [Architecture](docs/architecture.md#application-launch) owns scope and deferred artifact rename; `pnpm run verify-application-entrypoints` enforces it.
+Supported Node applications launch only through `dsh` profiles; application-package bins, demos, and public SDK argv escape hatches are forbidden. [Architecture](docs/architecture.md#application-launch) owns the launch set; `pnpm run verify-application-entrypoints` enforces it.
 
 ## Repository layout
 

+ 1 - 0
THIRD_PARTY_NOTICES.md

@@ -66,6 +66,7 @@ External packages that a workspace package resolves at runtime. The tier covers
 | [`eventsource-parser`](https://github.com/rexxars/eventsource-parser) | MIT |
 | [`fflate`](https://github.com/101arrowz/fflate) | MIT |
 | [`immer`](https://github.com/immerjs/immer) | MIT |
+| [`import-meta-resolve`](https://github.com/wooorm/import-meta-resolve) | MIT |
 | [`js-yaml`](https://github.com/nodeca/js-yaml) | MIT |
 | [`katex`](https://github.com/KaTeX/KaTeX) | MIT |
 | [`koffi`](https://github.com/Koromix/koffi) | MIT |

+ 1 - 1
apps/cli/src/bin.ts

@@ -41,7 +41,7 @@ switch (invocation.mode) {
   }
   case 'dump-config': {
     const { runDumpConfig } = await import('./dump-config.ts')
-    runDumpConfig(invocation.profile, invocation.defaultOnly, invocation.patches)
+    await runDumpConfig(invocation.profile, invocation.defaultOnly, invocation.patches)
     break
   }
   default:

+ 3 - 2
apps/cli/src/dump-config.ts

@@ -26,9 +26,10 @@ const NAME = 'dsh'
  * (the recovery diagnostic for a broken `cordis.patch.yml`, which is then
  * never parsed).
  * @param patches - `--patch` overlay paths, in argv order.
+ * @returns settlement after the profile is healed and the dump is written.
  */
-export function runDumpConfig(profile: string, defaultOnly: boolean, patches: readonly string[]): void {
-  const loaded = prepareProfile(profile, !defaultOnly)
+export async function runDumpConfig(profile: string, defaultOnly: boolean, patches: readonly string[]): Promise<void> {
+  const loaded = await prepareProfile(profile, !defaultOnly)
   const layers: ConfigDumpLayer[] = loaded.layers.map(layer => ({
     label: layer.packageName,
     patches: layer.patches,

+ 6 - 6
apps/cli/src/profile-boot.ts

@@ -115,8 +115,8 @@ export function resolveTelemetryPatch(disabledEnv: string | undefined, hasRow: b
  * @param userLayer - `false` skips parsing `cordis.patch.yml` (the default dump).
  * @returns the loaded profile.
  */
-export function prepareProfile(name: string, userLayer = true): Profile {
-  healProfilesModuleFallback(INSTALL_ANCHOR)
+export async function prepareProfile(name: string, userLayer = true): Promise<Profile> {
+  await healProfilesModuleFallback(INSTALL_ANCHOR)
   const profile = loadProfile(NAME, name, INSTALL_ANCHOR, undefined, { userLayer })
   writeFileSync(join(profile.dir, PROFILE_ROOT_FILENAME), PROFILE_ROOT_CONFIG)
   return profile
@@ -154,11 +154,11 @@ function allPatches(composed: ComposedProfile): PatchOptions[] {
  * @param patchFiles - `--patch` overlay paths, in argv order.
  * @returns the profile and its patch layers.
  */
-function composeProfile(
+async function composeProfile(
   name: string,
   patchFiles: readonly string[],
-): ComposedProfile {
-  const profile = prepareProfile(name)
+): Promise<ComposedProfile> {
+  const profile = await prepareProfile(name)
   const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? []
   const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
   const bundlePatches = profile.layers.flatMap(layer => layer.patches)
@@ -207,7 +207,7 @@ function suppressShutdownError(ctx: Context, signal: AbortSignal, error: unknown
  * @returns the settled root context and the shutdown controller.
  */
 export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Context; shutdown: ProcessShutdown }> {
-  const composed = composeProfile(options.profile, options.patchFiles)
+  const composed = await composeProfile(options.profile, options.patchFiles)
   const app: { current?: Context } = {}
   const appReady = createAppReady()
   const shutdown = createProcessShutdown(async () => { await app.current?.fiber.dispose() })

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

@@ -107,7 +107,7 @@ async function bootWeb(
   // 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)
-  healProfilesModuleFallback(INSTALL_ANCHOR, home)
+  await healProfilesModuleFallback(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

+ 1 - 1
apps/web/tests/scaffold.ts

@@ -519,7 +519,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
     // The production module-resolution setup: an empty profile root inside the temp
     // harness home, with bare plugin names resolving through the flat module
     // fallback the launcher heals under <home>/profiles.
-    healProfilesModuleFallback(INSTALL_ANCHOR, harnessHome)
+    await healProfilesModuleFallback(INSTALL_ANCHOR, harnessHome)
     const profileDir = join(harnessHome, 'profiles', 'scaffold')
     await mkdir(profileDir, { recursive: true })
     const rootConfig = join(profileDir, 'cordis.yml')

+ 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: 81c53abb799a91a2f7601f7506defbe9dc465820
-README.zh.md: 0d9e0707e0e809f4b2db0643a04619573107941a
+README.md: f48abfa445e4ac604b5e1bc694f8bae395b566e7
+README.zh.md: e903c74fb12fee0fca1d201d217dd28be8b7e08d

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
packages/boot/app-boot/README.md


Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 0 - 0
packages/boot/app-boot/README.zh.md


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

@@ -32,6 +32,8 @@
   ],
   "license": "MIT",
   "dependencies": {
+    "@deepseek-ai/dsh-atomic-write": "workspace:^",
+    "import-meta-resolve": "^4.2.0",
     "js-yaml": "^4.2.0"
   },
   "peerDependencies": {

+ 63 - 29
packages/boot/app-boot/src/profile.ts

@@ -27,10 +27,12 @@ import {
   existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, rmSync, symlinkSync, unlinkSync, writeFileSync,
 } from 'node:fs'
 import { basename, dirname, join } from 'node:path'
-import { pathToFileURL } from 'node:url'
+import { fileURLToPath, pathToFileURL } from 'node:url'
+import { withFileLock } from '@deepseek-ai/dsh-atomic-write'
 import type { EntryOptions } from '@deepseek-ai/cordis-plugin-loader'
 import { applyEntryPatches, type PatchOptions } from '@deepseek-ai/cordis-plugin-include'
 import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
+import { resolve as resolveImport } from 'import-meta-resolve'
 import { loadOverlayPatches } from './index.ts'
 
 /** Directory under the Harness home holding every profile. */
@@ -205,7 +207,16 @@ export function initProfile(
   if (!existsSync(workspacePath)) writeFileSync(workspacePath, PROFILE_PNPM_WORKSPACE)
 }
 
-/** Ensure `link` is a symlink to `target`, replacing a wrong or dangling link; a real directory throws. */
+function readModuleProxyRecord(link: string): ModuleProxyRecord | undefined {
+  try {
+    return JSON.parse(readFileSync(join(link, 'package.json'), 'utf8')) as ModuleProxyRecord
+  } catch {
+    // Missing or invalid metadata is not managed state; callers reject it.
+    return undefined
+  }
+}
+
+/** Ensure `link` is a symlink to `target`, replacing a wrong link or a dsh-managed packaged proxy. */
 function ensureSymlink(link: string, target: string): void {
   let stat
   try {
@@ -217,12 +228,19 @@ function ensureSymlink(link: string, target: string): void {
   }
   if (stat !== undefined) {
     if (!stat.isSymbolicLink()) {
-      throw new Error(`dsh: ${link} exists and is not a symlink; remove it so dsh can manage the installation fallback`)
+      const existing = stat.isDirectory() ? readModuleProxyRecord(link) : undefined
+      if (existing?.dsh?.moduleFallback?.targets === undefined) {
+        throw new Error(`dsh: ${link} exists and is not a symlink or dsh-managed module proxy; remove it so dsh can manage the installation fallback`)
+      }
+      rmSync(link, { recursive: true })
+      stat = undefined
+    }
+    if (stat !== undefined) {
+      if (readlinkSync(link) === target) return
+      // unlink deletes the reparse point itself on Windows too; rmSync treats a
+      // junction as a directory and throws EISDIR unless recursive.
+      unlinkSync(link)
     }
-    if (readlinkSync(link) === target) return
-    // unlink deletes the reparse point itself on Windows too; rmSync treats a
-    // junction as a directory and throws EISDIR unless recursive.
-    unlinkSync(link)
   }
   try {
     symlinkSync(target, link, 'junction')
@@ -258,29 +276,40 @@ function isPackagedExecutable(): boolean {
   return (process as NodeJS.Process & { pkg?: unknown }).pkg !== undefined
 }
 
-/** Resolve one package specifier from the dsh installation. */
-function packageEntryFromAnchor(anchor: string, specifier: string): string | undefined {
+/** Resolve one package export with the ESM conditions used by the generated proxy. */
+function packageEntryFromPackage(packageName: string, packageDir: string, specifier: string): string {
   try {
-    return createRequire(anchor).resolve(specifier)
-  } catch {
-    return undefined
+    const resolved = resolveImport(specifier, pathToFileURL(join(packageDir, 'package.json')).href)
+    if (!resolved.startsWith('file:') || !existsSync(fileURLToPath(resolved))) {
+      throw new Error(`resolved to missing or non-file URL ${resolved}`)
+    }
+    return resolved
+  } catch (error) {
+    throw new Error(`dsh: cannot resolve ESM export ${specifier} from installed package ${packageName}`, { cause: error })
   }
 }
 
 /** Resolve every explicit runtime export that an out-of-tree plugin can import. */
 function packageProxySource(
-  installAnchor: string,
   packageName: string,
   packageDir: string,
 ): { version: string; targets: Record<string, string> } {
   const manifest = JSON.parse(readFileSync(join(packageDir, 'package.json'), 'utf8')) as {
     exports?: unknown
+    main?: unknown
     version?: unknown
   }
   if (typeof manifest.version !== 'string' || manifest.version.length === 0) {
     throw new Error(`dsh: installed package ${packageName} must declare a non-empty version`)
   }
   const declared = manifest.exports
+  if (declared === undefined) {
+    const entry = join(packageDir, typeof manifest.main === 'string' ? manifest.main : 'index.js')
+    if (!existsSync(entry)) {
+      throw new Error(`dsh: installed package ${packageName} main entry is missing at ${entry}`)
+    }
+    return { version: manifest.version, targets: { '.': pathToFileURL(entry).href } }
+  }
   const subpaths = declared !== null && typeof declared === 'object' && !Array.isArray(declared)
     && Object.keys(declared).some(key => key.startsWith('.'))
     ? Object.keys(declared).filter(key => key === '.' || (key.startsWith('./') && !key.includes('*') && key !== './package.json'))
@@ -288,8 +317,7 @@ function packageProxySource(
   const targets: Record<string, string> = {}
   for (const subpath of subpaths) {
     const specifier = subpath === '.' ? packageName : packageName + subpath.slice(1)
-    const entry = packageEntryFromAnchor(installAnchor, specifier)
-    if (entry !== undefined) targets[subpath] = pathToFileURL(entry).href
+    targets[subpath] = packageEntryFromPackage(packageName, packageDir, specifier)
   }
   return { version: manifest.version, targets }
 }
@@ -328,18 +356,13 @@ function ensureModuleProxy(
     stat = undefined
   }
   if (stat !== undefined) {
-    const marker = join(link, 'package.json')
-    let existing: ModuleProxyRecord | undefined
-    try {
-      existing = JSON.parse(readFileSync(marker, 'utf8')) as ModuleProxyRecord
-    } catch {
-      existing = undefined
-    }
+    const existing = readModuleProxyRecord(link)
     if (existing?.dsh?.moduleFallback?.targets === undefined) {
       throw new Error(`dsh: ${link} exists and is not a dsh-managed module proxy; remove it so dsh can manage the installation fallback`)
     }
     if (existing.version === version
-      && JSON.stringify(existing.dsh.moduleFallback.targets) === JSON.stringify(targets)) return
+      && JSON.stringify(existing.dsh.moduleFallback.targets) === JSON.stringify(targets)
+      && Object.keys(targets).every((_, index) => existsSync(join(link, `entry-${index}.js`)))) return
     rmSync(link, { recursive: true })
   }
   mkdirSync(link, { recursive: true })
@@ -357,10 +380,12 @@ function ensureModuleProxy(
  * Maintain the flat module fallback `$DSH_HOME/profiles/node_modules`: one
  * entry per package in the dsh app's resolvable dependency CLOSURE (BFS
  * over `dependencies` from the app manifest), each resolved from its own
- * installation location. Plain Node uses symlinks. A pkg executable writes
- * small ESM proxy packages instead because the host filesystem cannot follow
- * a symlink into pkg's virtual `/snapshot` tree; the proxy re-exports the
- * virtual URL, preserving the executable's single module instance. Node's
+ * installation location. Plain Node uses symlinks. A pkg executable resolves
+ * exports under ESM import conditions and writes small proxy packages because
+ * the host filesystem cannot follow a symlink into pkg's virtual `/snapshot`
+ * tree; the proxy re-exports the virtual URL, preserving the executable's
+ * single module instance. One cross-process writer lock prevents partial
+ * proxies and serializes carrier transitions. Node's
  * parent-directory walk from any profile finds this
  * directory after the profile's own `node_modules`, so every in-box plugin
  * resolves without pnpm ever managing it — the exact "bundles come from the
@@ -375,11 +400,20 @@ function ensureModuleProxy(
  * reused because resolution cannot discover it.
  * @param installAnchor - absolute path of the dsh app's package.json.
  * @param home - the Harness home; defaults to {@link resolveDshHome}.
+ * @returns settlement after the locked fallback generation is complete.
  */
-export function healProfilesModuleFallback(installAnchor: string, home: string = resolveDshHome()): void {
+export async function healProfilesModuleFallback(installAnchor: string, home: string = resolveDshHome()): Promise<void> {
   const profilesDir = join(home, PROFILES_DIR)
   const modulesDir = join(profilesDir, 'node_modules')
   mkdirSync(modulesDir, { recursive: true })
+  await withFileLock(modulesDir, () => {
+    healProfilesModuleFallbackLocked(installAnchor, modulesDir)
+    return Promise.resolve()
+  })
+}
+
+/** Heal one module-fallback generation while the cross-process writer lock is held. */
+function healProfilesModuleFallbackLocked(installAnchor: string, modulesDir: string): void {
   const appManifest = JSON.parse(readFileSync(installAnchor, 'utf8')) as ProfileManifest
   const links = new Map<string, string>()
   /* v8 ignore next -- a real app manifest always declares its name */
@@ -407,7 +441,7 @@ export function healProfilesModuleFallback(installAnchor: string, home: string =
     const link = join(modulesDir, packageName)
     mkdirSync(dirname(link), { recursive: true })
     if (isPackagedExecutable()) {
-      const source = packageProxySource(installAnchor, packageName, target)
+      const source = packageProxySource(packageName, target)
       if (Object.keys(source.targets).length > 0) {
         ensureModuleProxy(link, packageName, source.version, source.targets)
       }

+ 136 - 28
packages/boot/app-boot/tests/profile.spec.ts

@@ -4,9 +4,10 @@
  * empty-root composition, and the installation module-fallback healing.
  */
 
-import { lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
+import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs'
 import { tmpdir } from 'node:os'
 import { join } from 'node:path'
+import { withFileLock } from '@deepseek-ai/dsh-atomic-write'
 import { describe, expect, it } from 'vitest'
 import {
   composeEntries,
@@ -271,7 +272,7 @@ describe('composeEntries', () => {
 })
 
 describe('healProfilesModuleFallback', () => {
-  it('links the app and bundle dependency surface flat under profiles/node_modules', () => {
+  it('links the app and bundle dependency surface flat under profiles/node_modules', async () => {
     const anchor = stageInstallation({
       'bundle-a': { patch: '[]\n', deps: { 'dep-of-a': '0.0.0', 'ghost-dep': '0.0.0' } },
       'plain-lib': {},
@@ -285,7 +286,7 @@ describe('healProfilesModuleFallback', () => {
     mkdirSync(join(modules, 'dep-of-a'), { recursive: true })
     writeFileSync(join(modules, 'dep-of-a', 'package.json'), JSON.stringify({ name: 'dep-of-a', version: '0.0.0' }))
     const home = tmp()
-    healProfilesModuleFallback(anchor, home)
+    await healProfilesModuleFallback(anchor, home)
     const fallback = join(home, 'profiles', 'node_modules')
     // App deps, the bundle's own deps, and the bundle itself are linked; the
     // plain library is linked as an app dep (harmless), the app itself too.
@@ -293,42 +294,67 @@ describe('healProfilesModuleFallback', () => {
       expect(lstatSync(join(fallback, name)).isSymbolicLink(), name).toBe(true)
     }
     // Idempotent, and a moved target is re-pointed.
-    healProfilesModuleFallback(anchor, home)
+    await healProfilesModuleFallback(anchor, home)
     const before = readlinkSync(join(fallback, 'dep-of-a'))
     expect(before).toContain('dep-of-a')
   })
 
-  it('throws when a fallback entry is a real directory', () => {
+  it('throws when a fallback entry is a foreign file or directory', async () => {
     const anchor = stageInstallation({})
-    const home = tmp()
-    mkdirSync(join(home, 'profiles', 'node_modules', 'dsh-app'), { recursive: true })
-    expect(() => { healProfilesModuleFallback(anchor, home) }).toThrow('is not a symlink')
+    for (const kind of ['file', 'directory']) {
+      const home = tmp()
+      const entry = join(home, 'profiles', 'node_modules', 'dsh-app')
+      mkdirSync(join(entry, '..'), { recursive: true })
+      if (kind === 'directory') mkdirSync(entry)
+      else writeFileSync(entry, '')
+      await expect(healProfilesModuleFallback(anchor, home)).rejects.toThrow('is not a symlink')
+    }
   })
 
-  it('replaces a wrong symlink', () => {
+  it('replaces a wrong symlink', async () => {
     const anchor = stageInstallation({})
     const home = tmp()
     const fallback = join(home, 'profiles', 'node_modules')
     mkdirSync(fallback, { recursive: true })
     symlinkSync(tmp(), join(fallback, 'dsh-app'), 'junction')
-    healProfilesModuleFallback(anchor, home)
+    await healProfilesModuleFallback(anchor, home)
     expect(readlinkSync(join(fallback, 'dsh-app'))).toContain('app')
   })
 
-  it('tolerates losing the concurrent-heal race to an identical link and rejects a different one', () => {
-    // The EEXIST arm: a second process wrote the link between our lstat miss
-    // and symlinkSync. Simulated by pre-creating the correct link and calling
-    // the internal path through a stale-lstat shim is not possible from
-    // outside, so probe the observable contract: healing twice concurrently
-    // is a no-op, and a foreign REAL directory still fails loud.
+  it('serializes concurrent healers and retains the identical link', async () => {
     const anchor = stageInstallation({})
     const home = tmp()
-    healProfilesModuleFallback(anchor, home)
-    healProfilesModuleFallback(anchor, home) // second healer sees the correct link
+    await Promise.all([
+      healProfilesModuleFallback(anchor, home),
+      healProfilesModuleFallback(anchor, home),
+    ])
     const fallback = join(home, 'profiles', 'node_modules')
     expect(lstatSync(join(fallback, 'dsh-app')).isSymbolicLink()).toBe(true)
   })
 
+  it('waits for the module-fallback writer lock before publishing entries', async () => {
+    const anchor = stageInstallation({})
+    const home = tmp()
+    const modules = join(home, 'profiles', 'node_modules')
+    mkdirSync(modules, { recursive: true })
+    let releaseLock: (() => void) | undefined
+    let reportLock: (() => void) | undefined
+    const lockHeld = new Promise<void>((resolve) => { reportLock = resolve })
+    const release = new Promise<void>((resolve) => { releaseLock = resolve })
+    const holder = withFileLock(modules, async () => {
+      reportLock?.()
+      await release
+    })
+    await lockHeld
+
+    const healer = healProfilesModuleFallback(anchor, home)
+    await new Promise(resolve => setTimeout(resolve, 20))
+    expect(existsSync(join(modules, 'dsh-app'))).toBe(false)
+    releaseLock?.()
+    await Promise.all([holder, healer])
+    expect(lstatSync(join(modules, 'dsh-app')).isSymbolicLink()).toBe(true)
+  })
+
   it('writes real ESM proxies for a packaged executable', async () => {
     const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
     const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
@@ -339,7 +365,7 @@ describe('healProfilesModuleFallback', () => {
     const home = tmp()
     Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
     try {
-      healProfilesModuleFallback(anchor, home)
+      await healProfilesModuleFallback(anchor, home)
       const fallback = join(home, 'profiles', 'node_modules')
       const proxy = join(fallback, 'bundle-a')
       expect(lstatSync(proxy).isDirectory()).toBe(true)
@@ -355,13 +381,79 @@ describe('healProfilesModuleFallback', () => {
       expect(proxyManifest.dsh.moduleFallback.targets['.']).toEqual(expect.stringContaining('/bundle-a/index.js'))
       await expect(import(join(proxy, 'entry-0.js'))).resolves.toMatchObject({ packageName: 'bundle-a' })
       await expect(import(join(proxy, 'entry-1.js'))).resolves.toMatchObject({ feature: 'proxied' })
-      healProfilesModuleFallback(anchor, home)
+      await healProfilesModuleFallback(anchor, home)
+    } finally {
+      delete (process as NodeJS.Process & { pkg?: unknown }).pkg
+    }
+  })
+
+  it('resolves import-only exports from each package installation', async () => {
+    const anchor = stageInstallation({
+      'bundle-a': { patch: '[]\n', deps: { 'nested-esm': '0.0.0' } },
+    })
+    const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
+    const bundleManifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
+    bundleManifest.exports = { '.': { import: './index.js' } }
+    writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(bundleManifest))
+    const nestedDir = join(bundleDir, 'node_modules', 'nested-esm')
+    mkdirSync(nestedDir, { recursive: true })
+    writeFileSync(join(nestedDir, 'package.json'), JSON.stringify({
+      name: 'nested-esm',
+      version: '0.0.0',
+      type: 'module',
+      exports: { import: './index.js' },
+    }))
+    writeFileSync(join(nestedDir, 'index.js'), 'export const nested = "proxied"\n')
+    const home = tmp()
+    Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
+    try {
+      await healProfilesModuleFallback(anchor, home)
+      const fallback = join(home, 'profiles', 'node_modules')
+      await expect(import(join(fallback, 'bundle-a', 'entry-0.js'))).resolves.toMatchObject({ packageName: 'bundle-a' })
+      await expect(import(join(fallback, 'nested-esm', 'entry-0.js'))).resolves.toMatchObject({ nested: 'proxied' })
     } finally {
       delete (process as NodeJS.Process & { pkg?: unknown }).pkg
     }
   })
 
-  it('requires a package version before writing a packaged proxy', () => {
+  it('uses the legacy index fallback when a package has no exports or main', async () => {
+    const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
+    const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
+    const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
+    delete manifest.main
+    writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
+    Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
+    try {
+      const home = tmp()
+      await healProfilesModuleFallback(anchor, home)
+      await expect(import(join(home, 'profiles', 'node_modules', 'bundle-a', 'entry-0.js')))
+        .resolves.toMatchObject({ packageName: 'bundle-a' })
+    } finally {
+      delete (process as NodeJS.Process & { pkg?: unknown }).pkg
+    }
+  })
+
+  it('fails loud on a missing legacy main entry or ESM export target', async () => {
+    for (const mode of ['legacy', 'exports']) {
+      const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
+      const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
+      const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
+      if (mode === 'legacy') delete manifest.main
+      else manifest.exports = { '.': { import: './missing.js' } }
+      writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
+      rmSync(join(bundleDir, 'index.js'))
+      Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
+      try {
+        await expect(healProfilesModuleFallback(anchor, tmp())).rejects.toThrow(
+          mode === 'legacy' ? 'main entry is missing' : 'cannot resolve ESM export bundle-a',
+        )
+      } finally {
+        delete (process as NodeJS.Process & { pkg?: unknown }).pkg
+      }
+    }
+  })
+
+  it('requires a package version before writing a packaged proxy', async () => {
     const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
     const bundleDir = join(anchor, '..', 'node_modules', 'bundle-a')
     const manifest = JSON.parse(readFileSync(join(bundleDir, 'package.json'), 'utf8')) as Record<string, unknown>
@@ -369,7 +461,7 @@ describe('healProfilesModuleFallback', () => {
     writeFileSync(join(bundleDir, 'package.json'), JSON.stringify(manifest))
     Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
     try {
-      expect(() => { healProfilesModuleFallback(anchor, tmp()) }).toThrow(
+      await expect(healProfilesModuleFallback(anchor, tmp())).rejects.toThrow(
         'installed package bundle-a must declare a non-empty version',
       )
     } finally {
@@ -377,23 +469,23 @@ describe('healProfilesModuleFallback', () => {
     }
   })
 
-  it('replaces plain-node links and stale managed proxies in packaged mode', () => {
+  it('replaces plain-node links and stale managed proxies in packaged mode', async () => {
     const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
     const home = tmp()
-    healProfilesModuleFallback(anchor, home)
+    await healProfilesModuleFallback(anchor, home)
     const proxy = join(home, 'profiles', 'node_modules', 'bundle-a')
     expect(lstatSync(proxy).isSymbolicLink()).toBe(true)
 
     Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
     try {
-      healProfilesModuleFallback(anchor, home)
+      await healProfilesModuleFallback(anchor, home)
       expect(lstatSync(proxy).isDirectory()).toBe(true)
       const stale = JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8')) as {
         version: string
       }
       stale.version = 'stale'
       writeFileSync(join(proxy, 'package.json'), JSON.stringify(stale))
-      healProfilesModuleFallback(anchor, home)
+      await healProfilesModuleFallback(anchor, home)
       expect(JSON.parse(readFileSync(join(proxy, 'package.json'), 'utf8'))).toMatchObject({
         version: '0.0.0',
       })
@@ -402,7 +494,23 @@ describe('healProfilesModuleFallback', () => {
     }
   })
 
-  it('rejects foreign packaged fallback directories with valid or invalid metadata', () => {
+  it('replaces a managed packaged proxy with a plain-node symlink', async () => {
+    const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
+    const home = tmp()
+    const fallback = join(home, 'profiles', 'node_modules', 'bundle-a')
+    Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
+    try {
+      await healProfilesModuleFallback(anchor, home)
+      expect(lstatSync(fallback).isDirectory()).toBe(true)
+    } finally {
+      delete (process as NodeJS.Process & { pkg?: unknown }).pkg
+    }
+
+    await healProfilesModuleFallback(anchor, home)
+    expect(lstatSync(fallback).isSymbolicLink()).toBe(true)
+  })
+
+  it('rejects foreign packaged fallback directories with valid or invalid metadata', async () => {
     const anchor = stageInstallation({ 'bundle-a': { patch: '[]\n' } })
     Object.defineProperty(process, 'pkg', { configurable: true, value: {} })
     try {
@@ -411,7 +519,7 @@ describe('healProfilesModuleFallback', () => {
         const proxy = join(home, 'profiles', 'node_modules', 'bundle-a')
         mkdirSync(proxy, { recursive: true })
         writeFileSync(join(proxy, 'package.json'), metadata)
-        expect(() => { healProfilesModuleFallback(anchor, home) }).toThrow(
+        await expect(healProfilesModuleFallback(anchor, home)).rejects.toThrow(
           'exists and is not a dsh-managed module proxy',
         )
       }

+ 3 - 0
packages/boot/app-boot/tsconfig.json

@@ -29,6 +29,9 @@
     {
       "path": "../../core/system-prompt"
     },
+    {
+      "path": "../../util/atomic-write"
+    },
     {
       "path": "../../util/launch-environment"
     },

+ 6 - 0
pnpm-lock.yaml

@@ -1145,6 +1145,12 @@ importers:
 
   packages/boot/app-boot:
     dependencies:
+      '@deepseek-ai/dsh-atomic-write':
+        specifier: workspace:^
+        version: link:../../util/atomic-write
+      import-meta-resolve:
+        specifier: ^4.2.0
+        version: 4.2.0
       js-yaml:
         specifier: ^4.2.0
         version: 4.2.0

+ 2 - 2
python/sdk/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 python/sdk/README.md
-README.md: edf5ced63f181bb17895d81cc060577916554305
-README.zh.md: e03a19e5b8f0cef2e2f9d86f8d91b8808e268ff4
+README.md: ef2ba1764aa93fc6499941dd372a1de94f33d454
+README.zh.md: b5c36b6ff0fbce303649ae92efd7e1edac954e82

+ 1 - 1
python/sdk/README.md

@@ -26,7 +26,7 @@ with DeepSeekHarness(
 print(result.final_response)
 ```
 
-`DeepSeekHarness` starts lazily and reuses its runtime until `close()` or context-manager exit. `cwd` is the agent workspace; `runtime_cwd` independently selects the subprocess working directory. Both become absolute before launch. `provider`, `model`, and optional positive `max_tokens` are sent during JSON-RPC initialization. `base_url` and `api_key` explicitly override `DEEPSEEK_BASE_URL` and `DEEPSEEK_API_KEY` in the child environment.
+`DeepSeekHarness` starts lazily and reuses its runtime until `close()` or context-manager exit. The initial profile handshake has an independent 10-second default bound through `initialize_timeout_seconds`; ordinary turns remain unbounded unless `request_timeout_seconds` is set. A timeout names the selected profile and includes retained runtime diagnostics. `cwd` is the agent workspace; `runtime_cwd` independently selects the subprocess working directory. Both become absolute before launch. `provider`, `model`, and optional positive `max_tokens` are sent during JSON-RPC initialization. `base_url` and `api_key` explicitly override `DEEPSEEK_BASE_URL` and `DEEPSEEK_API_KEY` in the child environment.
 
 ## Customize plugins
 

+ 1 - 1
python/sdk/README.zh.md

@@ -26,7 +26,7 @@ with DeepSeekHarness(
 print(result.final_response)
 ```
 
-`DeepSeekHarness` 延迟启动运行时,并在调用 `close()` 或退出上下文管理器前复用该进程。`cwd` 是 agent workspace;`runtime_cwd` 独立选择子进程工作目录。两者都会在启动前转成绝对路径。`provider`、`model` 和可选的正整数 `max_tokens` 通过 JSON-RPC 初始化发送。`base_url` 与 `api_key` 会显式覆盖子进程环境中的 `DEEPSEEK_BASE_URL` 与 `DEEPSEEK_API_KEY`。
+`DeepSeekHarness` 延迟启动运行时,并在调用 `close()` 或退出上下文管理器前复用该进程。首次 profile 握手通过 `initialize_timeout_seconds` 使用独立的 10 秒默认上限;普通轮次在未设置 `request_timeout_seconds` 时仍不设上限。超时诊断会指明所选 profile,并包含保留的运行时诊断。`cwd` 是 agent workspace;`runtime_cwd` 独立选择子进程工作目录。两者都会在启动前转成绝对路径。`provider`、`model` 和可选的正整数 `max_tokens` 通过 JSON-RPC 初始化发送。`base_url` 与 `api_key` 会显式覆盖子进程环境中的 `DEEPSEEK_BASE_URL` 与 `DEEPSEEK_API_KEY`。
 
 ## 自定义插件
 

+ 2 - 0
python/sdk/src/deepseek_harness/api.py

@@ -29,6 +29,7 @@ class DeepSeekHarnessConfig:
     patches: tuple[str, ...] = ()
     dsh_home: str | None = None
     env: dict[str, str] = field(default_factory=dict)
+    initialize_timeout_seconds: float = 10.0
     request_timeout_seconds: float | None = None
     shutdown_timeout_seconds: float | None = 1.0
     base_url: str | None = None
@@ -79,6 +80,7 @@ class DeepSeekHarness:
                 dsh_home=self.config.dsh_home,
                 cwd=runtime_cwd,
                 env=env,
+                initialize_timeout_seconds=self.config.initialize_timeout_seconds,
                 request_timeout_seconds=self.config.request_timeout_seconds,
                 shutdown_timeout_seconds=self.config.shutdown_timeout_seconds,
             ),

+ 10 - 1
python/sdk/src/deepseek_harness/client.py

@@ -31,6 +31,7 @@ class HarnessConfig:
     dsh_home: str | None = None
     cwd: str | None = None
     env: dict[str, str] | None = None
+    initialize_timeout_seconds: float = 10.0
     request_timeout_seconds: float | None = None
     shutdown_timeout_seconds: float | None = 1.0
     _launch_args: tuple[str, ...] | None = None
@@ -137,7 +138,15 @@ class HarnessClient:
         if max_tokens is not None:
             payload["maxTokens"] = max_tokens
         try:
-            return self.request("initialize", payload, response_model=InitializeResponse)
+            return self.request(
+                "initialize",
+                payload,
+                response_model=InitializeResponse,
+                timeout_seconds=self.config.initialize_timeout_seconds,
+            )
+        except TimeoutError as error:
+            self.close()
+            raise TimeoutError(f"{error}\nselected dsh profile {self.config.profile!r}") from error
         except BaseException as error:
             self.close()
             diagnostics = self._runtime_diagnostics()

+ 5 - 1
python/sdk/tests/test_client.py

@@ -731,7 +731,8 @@ time.sleep(60)
     with HarnessClient(
         HarnessConfig(
             _launch_args=(sys.executable, str(script)),
-            request_timeout_seconds=0.1,
+            profile="web",
+            initialize_timeout_seconds=0.1,
         )
     ) as client:
         start = time.monotonic()
@@ -740,6 +741,7 @@ time.sleep(60)
         except TimeoutError as exc:
             assert time.monotonic() - start < 2
             assert "bridge is still starting" in str(exc)
+            assert "profile 'web'" in str(exc)
         else:
             raise AssertionError("initialize should time out")
 
@@ -832,6 +834,8 @@ def test_public_signatures_omit_unsupported_wire_parameters() -> None:
     assert {"dsh_bin", "profile", "patches", "dsh_home"} <= set(
         HarnessConfig.__dataclass_fields__
     )
+    assert "initialize_timeout_seconds" in DeepSeekHarnessConfig.__dataclass_fields__
+    assert "initialize_timeout_seconds" in HarnessConfig.__dataclass_fields__
     for removed in ("cordis", "session_root", "runtime_bin", "bridge_bin", "launch_args_override"):
         assert removed not in DeepSeekHarnessConfig.__dataclass_fields__
         assert removed not in HarnessConfig.__dataclass_fields__

+ 27 - 0
scripts/build-exe-for-python-sdk-assets.spec.ts

@@ -0,0 +1,27 @@
+import { spawnSync } from 'node:child_process'
+import { resolve } from 'node:path'
+import { describe, expect, it } from 'vitest'
+
+const root = resolve(import.meta.dirname, '..')
+const script = resolve(root, 'scripts/build-exe-for-python-sdk.ts')
+
+describe('Python runtime executable assets', () => {
+  it('packages the dynamically resolved web frontend distribution', () => {
+    const result = spawnSync(process.execPath, [
+      '--import',
+      'tsx/esm',
+      script,
+      '--skip-build',
+      '--dry-run',
+      '--targets=node24-macos-arm64',
+    ], {
+      cwd: root,
+      encoding: 'utf8',
+      env: { ...process.env, npm_execpath: 'C:\\tools\\pnpm.cjs' },
+    })
+
+    expect(result.status).toBe(0)
+    expect(result.stdout).toContain('node_modules/@deepseek-ai/dsh-web-frontend/dist/**/*')
+    expect(result.stdout).toContain('node_modules/@deepseek-ai/dsh-skill-badge/assets/**/*')
+  })
+})

+ 5 - 0
scripts/build-exe-for-python-sdk.ts

@@ -47,6 +47,7 @@ const ASSET_GLOBS = [
   'node_modules/**/*.mjs',
   'node_modules/**/package.json',
   'node_modules/**/*.json',
+  // Package-owned Markdown includes runtime skill instructions and badge content.
   'node_modules/**/*.md',
   'node_modules/**/*.dylib',
   'node_modules/**/*.dll',
@@ -56,6 +57,10 @@ const ASSET_GLOBS = [
   'node_modules/**/*.wasm',
   'node_modules/**/*.yaml',
   'node_modules/**/*.yml',
+  // web-app builds this path dynamically, so pkg cannot discover the static frontend.
+  'node_modules/@deepseek-ai/dsh-web-frontend/dist/**/*',
+  // skill-badge resolves both Markdown and image resources through import.meta.url.
+  'node_modules/@deepseek-ai/dsh-skill-badge/assets/**/*',
 ]
 
 const PLATFORMS = ['linux', 'macos'] as const

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä