docs(browser-use): record the profile plugin instance-sharing decision
Adds the implemented Agent Note for the rule issue #4573 exposed: an out-of-tree plugin declares every host runtime whose cross-instance identity matters as a peer (plus dev), never as a dependency. Records the mechanism (dsh-scope mints its tag per module instance), what the duplicate does to a per-Agent registration, the alternatives that lost (mounting on agent.ctx, a shared Symbol.for tag, version alignment), and the open coverage gap: verify-package-dependencies selects no packages/experimental package, so tests/shared-host-runtimes.spec.ts is the only pin.
This commit is contained in:
1 parent
de8b10f8ab
commit
135104fb10
3 files changed
+80
No files matched your search
+6
@@ -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-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
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Agent Note: Profile plugins share the installation's host runtime instances
|
||||
|
||||
Status: implemented
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
`@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.
|
||||
|
||||
`tests/shared-host-runtimes.spec.ts` pins those sections for that package. `tests/host-runtime-duplication.spec.ts` reproduces what the rule prevents: one second copy of both packages puts each Agent's MCP tools in the global layer and rejects the second Agent's creation, while the shipped single-instance layout keeps the global layer empty and gives both Agents their own client.
|
||||
|
||||
The [published dependency faces](../process/2026-08-26-published-dependency-faces.md) note owns `verify-package-dependencies`: its `peerRequiredHostExports` table classifies the `dsh-scope` readers, and its package selection covers Client-faced packages and an explicit Host list. That gate selects no package under `packages/experimental/`, and it does not classify `createScope`; this note owns the rule for the out-of-tree plugin packages the gate does not reach.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**Mount the MCP client on `agent.ctx` instead of a scope this package mints.** It loses because the profile copy's `scopeOf` also cannot read the tag the installation wrote for `agent.ctx`, so the `serverName` reservation degrades to the process-global owner and the second Agent fails at that check — the error form seen on `0.1.6-alpha.1`. Removing the duplicate is the repair for both forms.
|
||||
|
||||
**Make `dsh-scope`'s tag a `Symbol.for`, so two copies agree.** That fixes the tag lookup alone. Scope parents and event-carrier keys stay per copy, so the chain walk and carrier admission of one copy remain blind to links the other recorded — a smaller visible failure with a more confusing cause. It also keeps the duplicate that the profile never needed.
|
||||
|
||||
**Match the profile's package versions to the Host's.** Equal versions still load two module instances from two directories, so the defect survives it.
|
||||
|
||||
## 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.
|
||||
- `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.
|
||||
- `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.
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# Agent Note: Profile 插件共享安装自带的宿主运行时实例
|
||||
|
||||
Status: implemented
|
||||
|
||||
[English](2026-09-18-profile-plugin-host-runtime-instances.md) | 中文
|
||||
|
||||
## Problem
|
||||
|
||||
Profile 解析裸模块名时,先看自己的 `node_modules`,再看安装。于是 profile 从 npm 装进来的 out-of-tree 插件,会为它经 `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 找到安装里的那一份实例。
|
||||
|
||||
`@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` 留在运行时依赖里。
|
||||
|
||||
`tests/shared-host-runtimes.spec.ts` 钉住这个包的分区。`tests/host-runtime-duplication.spec.ts` 复现这条规则拦下的问题:两份包各多一份副本时,每个 Agent 的 MCP 工具都落进全局层,第二个 Agent 的创建被拒;而实际发布的单实例布局下全局层为空,两个 Agent 各拿到自己的客户端。
|
||||
|
||||
[Published dependency faces](../process/2026-08-26-published-dependency-faces.zh.md) 拥有 `verify-package-dependencies`:它的 `peerRequiredHostExports` 表分类了 `dsh-scope` 的读取者,它的包选择覆盖 Client 面的包与一份显式 Host 名单。该 gate 不选择 `packages/experimental/` 下的任何包,也没有分类 `createScope`;gate 覆盖不到的 out-of-tree 插件包,由本记录拥有这条规则。
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
**把 MCP 客户端挂到 `agent.ctx`,而不是本包自造的作用域。** 它不成立,是因为 profile 那份副本的 `scopeOf` 同样读不到安装为 `agent.ctx` 写下的标记,`serverName` 保留会退化成进程全局所有者,第二个 Agent 在那一步就失败——即 `0.1.6-alpha.1` 上看到的错误形态。消除重复副本才是两种形态共同的修复。
|
||||
|
||||
**把 `dsh-scope` 的标记改成 `Symbol.for`,让两份副本互相认得。** 这只修好标记查找。作用域父链与事件载体键仍各留一份,一份副本的链式遍历与载体准入看不见另一份记录的关系——可见错误更小,成因更难定位,而且 profile 本不需要的那份重复依然存在。
|
||||
|
||||
**把 profile 的包版本对齐到 Host。** 版本相同,两个目录依旧加载出两份模块实例,问题照旧。
|
||||
|
||||
## Consequences
|
||||
|
||||
- 修复要生效,需要升级并重装 profile 里的插件依赖。在此之前,已装好的 profile 一直带着那份副本,Host 侧无法补救。
|
||||
- 这条规则让插件依赖「安装确实带了这个 peer」。两个包都是 `@deepseek-ai/dsh` 安装的依赖,fallback 能解析到;若某插件不在安装闭包内,它会直接加载失败,而不是带着第二份副本启动。
|
||||
- `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。
|
||||
- `verify-package-dependencies` 仍不选择 `packages/experimental/` 下的包,所以本包的分区只由 `tests/shared-host-runtimes.spec.ts` 钉住。扩大该 gate 的选择范围,就是本记录留下的覆盖缺口。
|
||||
Reference in new issue
Block a user