docs(browser-use): use the runtime resolution vocabulary in the instance note

The profile resolution lookup order note, merged today, owns the mechanism this
note describes and names it runtime resolution with an interception layer at
$DSH_HOME/profiles/node_modules. This note adopts that vocabulary and links the
owning record instead of restating the resolution order.

It also records the boundary the sibling experimental providers leave open: a
profile that installs one of them beside this package hoists their
dsh-mcp-client dependency into the profile's own node_modules, so this package's
peer resolves there and the two copies keep separate serverName reservations.
The scope split does not return, because none of them declares dsh-scope.
Issue #4628 tracks moving those declarations to peers.

Refs #4628
This commit is contained in:
mektpoy committed 2026-09-19 21:53:43 +08:00
1 parent 4167b60997
commit faa6986b2b
3 files changed
+10 -10

No files matched your search

@@ -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-09-18-profile-plugin-host-runtime-instances.md
2026-09-18-profile-plugin-host-runtime-instances.md: f8969b96627ba781fbac3ad45e3d92d62f1d4e78
2026-09-18-profile-plugin-host-runtime-instances.zh.md: a42b50eff9a651abae7cff6d467ea210a2edc985
2026-09-18-profile-plugin-host-runtime-instances.md: 76135318b3a935e4588c360df3210618a6c69216
2026-09-18-profile-plugin-host-runtime-instances.zh.md: b2229574876b0ad326348ababd6b6fca6f3a1ac2
@@ -6,13 +6,13 @@ English | [中文](2026-09-18-profile-plugin-host-runtime-instances.zh.md)
## Problem
A profile resolves a bare specifier against its own `node_modules` before the installation. A plugin the profile installs out-of-tree therefore receives its own copy of every package it reaches through a `dependencies` edge, while the Host that loads the plugin keeps the installation's copy. Most packages tolerate that. `@deepseek-ai/dsh-scope` does not: it tags a scoped context with a module-local `Symbol('dsh.scope')` and keeps scope parents and event-carrier keys in module-local tables, and every registry that reads a tag — tool registration, prompt sections, MCP resources, scoped events — compares it by identity. A profile copy's `createScope` writes a tag the installation's `scopeOf` cannot read, so a registration meant for one Agent lands in the process-global layer.
A plugin the profile installs out-of-tree resolves its bare specifiers along the ancestor `node_modules` chain, where the profile's own layer sits above the runtime resolution's interception layer ([lookup order](2026-09-19-profile-resolution-lookup-order.md)). It therefore receives its own copy of every package it reaches through a `dependencies` edge, while the Host that loads the plugin keeps the installation's copy. Most packages tolerate that. `@deepseek-ai/dsh-scope` does not: it tags a scoped context with a module-local `Symbol('dsh.scope')` and keeps scope parents and event-carrier keys in module-local tables, and every registry that reads a tag — tool registration, prompt sections, MCP resources, scoped events — compares it by identity. A profile copy's `createScope` writes a tag the installation's `scopeOf` cannot read, so a registration meant for one Agent lands in the process-global layer.
Issue #4573 is that failure in production. While this package reached both runtimes through `dependencies`, a profile that mounted the Playwright MCP browser provider installed a second `dsh-scope`: the first Agent's MCP tools registered globally, the second Agent's tool synchronization collided, and `failOnStartupError: true` turned that collision into a rejected Agent creation. Matching the profile's package versions to the Host's did not change the outcome, because a duplicate exists at every version.
## Decision
An out-of-tree plugin package declares every host runtime whose cross-instance identity matters in matching `peerDependencies` and `devDependencies`, never in `dependencies`. Profile pnpm settings are `nodeLinker: hoisted` with `autoInstallPeers: false`, so such a peer is not installed into the profile and Node reaches the installation's single instance through the Harness home's module fallback.
An out-of-tree plugin package declares every host runtime whose cross-instance identity matters in matching `peerDependencies` and `devDependencies`, never in `dependencies`. Profile pnpm settings are `nodeLinker: hoisted` with `autoInstallPeers: false`, so such a peer is not installed into the profile, and the import reaches the installation's single instance through the runtime resolution's interception layer at `$DSH_HOME/profiles/node_modules` ([lookup order](2026-09-19-profile-resolution-lookup-order.md)).
`@deepseek-ai/dsh-scope` carries that identity through `createScope` as well as through the readers `scopeOf`, `carrierKeyOf`, and `scopeTarget`: the writer mints the tag its readers compare. `@deepseek-ai/dsh-mcp-client` joins it because its live `serverName` reservations are module-local, so a second copy keeps a second reservation table for the same namespace. `@deepseek-ai/dsh-experimental-browser-use-runtime` therefore declares both as peers with matching dev entries and keeps `@deepseek-ai/schemastery` as its only runtime dependency, which carries no instance-local state.
@@ -31,7 +31,7 @@ The [published dependency faces](../process/2026-08-26-published-dependency-face
## Consequences
- A released fix reaches a profile when the plugin dependency is upgraded and reinstalled. An already-installed profile keeps its copies until then, and no Host-side change can repair it.
- The rule makes the plugin depend on the installation shipping the peer. Both packages are dependencies of the `@deepseek-ai/dsh` installation, so the fallback resolves them; a plugin outside the installation's closure would fail to load rather than start with a second copy.
- The rule makes the plugin depend on the installation shipping the peer. Both packages are dependencies of the `@deepseek-ai/dsh` installation, so the runtime resolution supplies them; a plugin outside the installation's closure would fail to load rather than start with a second copy.
- `docs/module-graph.md` renders these edges in its peer section, so the graph states the ownership boundary that the sections encode.
- Three sibling experimental providers (`browser-use-stagehand-native`, `computer-use-cua-driver-mcp`, `computer-use-cua-driver-native`) declare `@deepseek-ai/dsh-mcp-client` as a dependency. They mount no per-Agent MCP client, so this decision leaves them unchanged; giving one of them a per-Agent mount requires moving that declaration to peers first.
- Three sibling experimental providers (`browser-use-stagehand-native`, `computer-use-cua-driver-mcp`, `computer-use-cua-driver-native`) declare `@deepseek-ai/dsh-mcp-client` as a dependency. They mount no per-Agent MCP client, so their own use does not reproduce this decision's failure. A profile that installs one of them beside this package hoists that dependency into the profile's own `node_modules`, above the interception layer, so this package's peer resolves to that copy: the two `dsh-mcp-client` instances keep separate `serverName` reservations, and a name reserved on one is not detected on the other. The scope split does not return, because none of them declares `@deepseek-ai/dsh-scope`. Issue #4628 tracks moving those three declarations to peers with their own verification.
- `verify-package-dependencies` still selects no `packages/experimental/` package, so the sections of this package are pinned only by `tests/shared-host-runtimes.spec.ts`. Extending that gate's selection is the open coverage gap this note records.
@@ -6,13 +6,13 @@ Status: implemented
## Problem
Profile 解析裸模块名时,先看自己的 `node_modules`,再看安装。于是 profile 从 npm 装进来的 out-of-tree 插件,会为它经 `dependencies` 边到达的每个包各留一份自己的副本,而加载它的 Host 用的是安装里那一份。多数包可以容忍这种重复;`@deepseek-ai/dsh-scope` 不能:它用一个模块级 `Symbol('dsh.scope')` 给作用域上下文打标记,并把作用域父链和事件载体键保存在模块级表里,而每个读标记的注册表——工具注册、提示词 section、MCP 资源、作用域事件——都按同一性比较它。profile 那份副本的 `createScope` 写下的标记,安装那份的 `scopeOf` 读不到,于是本该属于某个 Agent 的注册落进了进程全局层。
profile 从 npm 装进来的 out-of-tree 插件,沿祖先 `node_modules` 链解析裸模块名,而 profile 自己那一层位于运行时解析(runtime resolution)的拦截层之上([解析顺序](2026-09-19-profile-resolution-lookup-order.zh.md))。于是它会为经 `dependencies` 边到达的每个包各留一份自己的副本,而加载它的 Host 用的是安装里那一份。多数包可以容忍这种重复;`@deepseek-ai/dsh-scope` 不能:它用一个模块级 `Symbol('dsh.scope')` 给作用域上下文打标记,并把作用域父链和事件载体键保存在模块级表里,而每个读标记的注册表——工具注册、提示词 section、MCP 资源、作用域事件——都按同一性比较它。profile 那份副本的 `createScope` 写下的标记,安装那份的 `scopeOf` 读不到,于是本该属于某个 Agent 的注册落进了进程全局层。
Issue #4573 就是这个问题在生产里的形态。当本包经 `dependencies` 到达这两个运行时时,挂载 Playwright MCP 浏览器提供方的 profile 多装了一份 `dsh-scope`:第一个 Agent 的 MCP 工具注册到全局层,第二个 Agent 的工具同步撞名,`failOnStartupError: true` 把这次撞名放大成 Agent 创建失败。把 profile 的包版本对齐到 Host 并不能改变结果,因为无论哪个版本,副本都是第二份模块实例。
## Decision
Out-of-tree 插件包,凡是跨实例同一性要紧的宿主运行时,一律写成匹配的 `peerDependencies` 与 `devDependencies`,绝不写进 `dependencies`。Profile 的 pnpm 配置是 `nodeLinker: hoisted` 加 `autoInstallPeers: false`,这类 peer 不会装进 profile,Node 会经 Harness home 的模块 fallback 找到安装里的那一份实例。
Out-of-tree 插件包,凡是跨实例同一性要紧的宿主运行时,一律写成匹配的 `peerDependencies` 与 `devDependencies`,绝不写进 `dependencies`。Profile 的 pnpm 配置是 `nodeLinker: hoisted` 加 `autoInstallPeers: false`,这类 peer 不会装进 profile;导入走到 `$DSH_HOME/profiles/node_modules` 时,由运行时解析(runtime resolution)的拦截层提供安装里的那一份实例([解析顺序](2026-09-19-profile-resolution-lookup-order.zh.md))。
`@deepseek-ai/dsh-scope` 的同一性既在读取者 `scopeOf`、`carrierKeyOf`、`scopeTarget` 上,也在写入者 `createScope` 上:标记由写入者铸造,再由读取者比较。`@deepseek-ai/dsh-mcp-client` 同属这一类,因为它在线的 `serverName` 保留表是模块级的,第二份副本会为同一命名空间保留第二张表。因此 `@deepseek-ai/dsh-experimental-browser-use-runtime` 把两者声明为 peer(并补匹配的 dev 条目),只把不携带实例内状态的 `@deepseek-ai/schemastery` 留在运行时依赖里。
@@ -31,7 +31,7 @@ Out-of-tree 插件包,凡是跨实例同一性要紧的宿主运行时,一
## Consequences
- 修复要生效,需要升级并重装 profile 里的插件依赖。在此之前,已装好的 profile 一直带着那份副本,Host 侧无法补救。
- 这条规则让插件依赖「安装确实带了这个 peer」。两个包都是 `@deepseek-ai/dsh` 安装的依赖,fallback 能解析到;若某插件不在安装闭包内,它会直接加载失败,而不是带着第二份副本启动。
- 这条规则让插件依赖「安装确实带了这个 peer」。两个包都是 `@deepseek-ai/dsh` 安装的依赖,运行时解析能提供它们;若某插件不在安装闭包内,它会直接加载失败,而不是带着第二份副本启动。
- `docs/module-graph.md` 把这些边画进 peer 区,图与分区表达的是同一条所有权边界。
- 另有三个实验提供方(`browser-use-stagehand-native`、`computer-use-cua-driver-mcp`、`computer-use-cua-driver-native`)把 `@deepseek-ai/dsh-mcp-client` 写成依赖。它们不按 Agent 挂 MCP 客户端,本决策不动它们;若将来其中一个改成按 Agent 挂载,必须先把那条声明移到 peer。
- 另有三个实验提供方(`browser-use-stagehand-native`、`computer-use-cua-driver-mcp`、`computer-use-cua-driver-native`)把 `@deepseek-ai/dsh-mcp-client` 写成依赖。它们不按 Agent 挂 MCP 客户端,因此自身不会触发本决策要防的故障。若一个 profile 把其中之一与本包一起安装,hoisted 会把那份依赖平铺到 profile 自己的 `node_modules`,位置高于拦截层,于是本包的 peer 解析到那份副本:两份 `dsh-mcp-client` 各存一张 `serverName` 保留表,一份上保留的名字在另一份上不再被检测。作用域标记的分裂不会回来,因为它们都没有把 `@deepseek-ai/dsh-scope` 写成依赖。Issue #4628 记录把这三处声明改为 peer 并各自验证的后续工作。
- `verify-package-dependencies` 仍不选择 `packages/experimental/` 下的包,所以本包的分区只由 `tests/shared-host-runtimes.spec.ts` 钉住。扩大该 gate 的选择范围,就是本记录留下的覆盖缺口。