Procházet zdrojové kódy

Merge branch 'feat/plugin-mgmt-3-settings' into feat/plugin-mgmt-4-web

Yichen Jiang před 3 týdny
rodič
revize
eb2ee55f83

+ 2 - 2
.agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.md
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.md
-2026-09-04-plugin-manager-over-the-profile-runtime.md: 4fcf693beb74949b3813fe24c6255ec3d9f5ac36
-2026-09-04-plugin-manager-over-the-profile-runtime.zh.md: b396aa9bb23b66d60119eb2868539bec5b97ad13
+2026-09-04-plugin-manager-over-the-profile-runtime.md: af44e57a6b1be94cd0df24c3dfeede50a1c8285d
+2026-09-04-plugin-manager-over-the-profile-runtime.zh.md: 5754187f79e167fe9f4a54dbccc8c6dc782edff2

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.md

@@ -14,7 +14,7 @@ Installing a plugin was a terminal-only act: `dsh plugin --profile web add <spec
 
 
 **Enablement is the Loader's transaction.** `enable` puts the bundle in the layer list and calls `profileRuntime.recompose({ reloadBundles: true })`, after `healProfilesModuleFallback` has linked the packages the bundle carries. A rejected recomposition — a `boot`-stage bundle whose row throws — is the Loader rolling back to the tree that was running; the manager restores the list and reports `plugins/enable-failed`. A `runtime`-stage bundle whose row fails is isolated by the contained group and reported per row. Because the boot audit does not run again, the manager calls `recordContainedStates` after a live recomposition, and `ContainedGroup.create` now records a row that resolved in the pending state instead of clearing it — a reload re-creates every row of a group, and a waiting row must keep its record through that. `retry` is disable then enable: the Loader's update leaves an unchanged row alone, so only leaving and returning restarts a failed isolated row.
 **Enablement is the Loader's transaction.** `enable` puts the bundle in the layer list and calls `profileRuntime.recompose({ reloadBundles: true })`, after `healProfilesModuleFallback` has linked the packages the bundle carries. A rejected recomposition — a `boot`-stage bundle whose row throws — is the Loader rolling back to the tree that was running; the manager restores the list and reports `plugins/enable-failed`. A `runtime`-stage bundle whose row fails is isolated by the contained group and reported per row. Because the boot audit does not run again, the manager calls `recordContainedStates` after a live recomposition, and `ContainedGroup.create` now records a row that resolved in the pending state instead of clearing it — a reload re-creates every row of a group, and a waiting row must keep its record through that. `retry` is disable then enable: the Loader's update leaves an unchanged row alone, so only leaving and returning restarts a failed isolated row.
 
 
-**pnpm runs the way the CLI runs it.** Through `node:child_process` with the parent environment and `shell` on Windows, not through the subprocess seam: the seam scrubs secret-shaped variables pnpm needs for registries and proxies and has no shell mode for the `.cmd` shim. Output streams as `plugins/install-log` chunks under a job id, each naming the command line, with pnpm's colours kept for the Web dialog's terminal and stripped for a CLI whose stdout is not a terminal; a non-zero exit, a spawn error, or the timeout is `plugins/install-failed` with the log tail. New packages are probed and left disabled unless the caller asked for `enable`.
+**pnpm runs the way the CLI runs it.** Through `node:child_process` with the parent environment and `shell` on Windows, not through the subprocess seam: the seam scrubs secret-shaped variables pnpm needs for registries and proxies and has no shell mode for the `.cmd` shim. Output streams as `plugins/install-log` chunks under a job id, each naming the command line and the profile directory, with pnpm's colours kept for the Web dialog's terminal and stripped for a CLI whose stdout is not a terminal; a non-zero exit, a spawn error, or the timeout is `plugins/install-failed` with the log tail. New packages are probed and left disabled unless the caller asked for `enable`.
 
 
 **A successful `pnpm add` is not an installed plugin.** The manifest is snapshotted before the run and restored when pnpm fails, so a failed add leaves no dependency behind. Each package pnpm added is then judged: one that declares neither a bundle nor a plugin module, or a bundle whose row id a composed layer already owns (`claimLayerIds` over the current layers plus the candidate), is removed again with `pnpm remove` and reported under `removed` with its reason; a package the probe refused stays, because the view can explain it and `retry` can try again. The manager runs one mutation at a time and refuses a second with `plugins/busy` rather than queueing it — every write races on the manifest, the user layers, or `node_modules` otherwise — and `add` and `uninstall` refuse with `plugins/agents-running` while any agent is running, because pnpm rewrites the directory those sessions import from. The three guards follow the community `dshmarket` manager, which learned each of them from a bug.
 **A successful `pnpm add` is not an installed plugin.** The manifest is snapshotted before the run and restored when pnpm fails, so a failed add leaves no dependency behind. Each package pnpm added is then judged: one that declares neither a bundle nor a plugin module, or a bundle whose row id a composed layer already owns (`claimLayerIds` over the current layers plus the candidate), is removed again with `pnpm remove` and reported under `removed` with its reason; a package the probe refused stays, because the view can explain it and `retry` can try again. The manager runs one mutation at a time and refuses a second with `plugins/busy` rather than queueing it — every write races on the manifest, the user layers, or `node_modules` otherwise — and `add` and `uninstall` refuse with `plugins/agents-running` while any agent is running, because pnpm rewrites the directory those sessions import from. The three guards follow the community `dshmarket` manager, which learned each of them from a bug.
 
 

+ 1 - 1
.agents/notes/implemented/architecture/2026-09-04-plugin-manager-over-the-profile-runtime.zh.md

@@ -14,7 +14,7 @@ Status: implemented
 
 
 **启用就是 Loader 的事务。** `enable` 把组合包放进层列表,在 `healProfilesModuleFallback` 链接好该组合包携带的包之后调用 `profileRuntime.recompose({ reloadBundles: true })`。被拒绝的重新组合——`boot` 阶段而行抛错的组合包——就是 Loader 回滚到原本运行的树;管理器恢复层列表并报告 `plugins/enable-failed`。`runtime` 阶段而行失败的组合包由受控组隔离并逐行报告。由于启动审计不会再跑一次,管理器在在线重新组合之后调用 `recordContainedStates`,而 `ContainedGroup.create` 现在把以 pending 状态完成创建的行记录下来而不是清除——重载会重新创建组里的每一行,等待中的行必须带着记录穿过这一过程。`retry` 是先停用再启用:Loader 的更新不碰未改变的行,只有离开再回来才能重启一条失败的隔离行。
 **启用就是 Loader 的事务。** `enable` 把组合包放进层列表,在 `healProfilesModuleFallback` 链接好该组合包携带的包之后调用 `profileRuntime.recompose({ reloadBundles: true })`。被拒绝的重新组合——`boot` 阶段而行抛错的组合包——就是 Loader 回滚到原本运行的树;管理器恢复层列表并报告 `plugins/enable-failed`。`runtime` 阶段而行失败的组合包由受控组隔离并逐行报告。由于启动审计不会再跑一次,管理器在在线重新组合之后调用 `recordContainedStates`,而 `ContainedGroup.create` 现在把以 pending 状态完成创建的行记录下来而不是清除——重载会重新创建组里的每一行,等待中的行必须带着记录穿过这一过程。`retry` 是先停用再启用:Loader 的更新不碰未改变的行,只有离开再回来才能重启一条失败的隔离行。
 
 
-**pnpm 按 CLI 的方式运行。** 经 `node:child_process`、带父进程环境、Windows 上开 `shell`,而不经 subprocess seam:seam 会清洗 pnpm 访问 registry 与代理所需的形似密钥的变量,也没有解析 `.cmd` shim 的 shell 模式。输出以某个 job id 下的 `plugins/install-log` 分块流式发出,每块写明命令行,pnpm 的颜色为 Web 对话框的终端保留、对 stdout 不是终端的 CLI 去掉;非零退出、spawn 错误或超时即带日志尾部的 `plugins/install-failed`。新包被探测并保持停用,除非调用方要求 `enable`。
+**pnpm 按 CLI 的方式运行。** 经 `node:child_process`、带父进程环境、Windows 上开 `shell`,而不经 subprocess seam:seam 会清洗 pnpm 访问 registry 与代理所需的形似密钥的变量,也没有解析 `.cmd` shim 的 shell 模式。输出以某个 job id 下的 `plugins/install-log` 分块流式发出,每块写明命令行与 profile 目录,pnpm 的颜色为 Web 对话框的终端保留、对 stdout 不是终端的 CLI 去掉;非零退出、spawn 错误或超时即带日志尾部的 `plugins/install-failed`。新包被探测并保持停用,除非调用方要求 `enable`。
 
 
 **`pnpm add` 成功不等于装好了插件。** 运行前先给 manifest 拍快照,pnpm 失败时恢复,失败的 add 不会留下依赖。之后逐个裁决 pnpm 加进来的包:既不声明组合包也不声明插件模块的,或者行 id 已被已组合层占有的组合包(对当前各层加候选层跑 `claimLayerIds`),再以 `pnpm remove` 移除并连同原因报在 `removed` 里;探针拒绝的包保留,因为视图能解释它、`retry` 还能再试。管理器一次只跑一个变更,第二个以 `plugins/busy` 拒绝而不是排队——否则每次写入都会在 manifest、用户层或 `node_modules` 上竞争——`add` 与 `uninstall` 在任一 agent 运行时以 `plugins/agents-running` 拒绝,因为 pnpm 会重写那些会话正在 import 的目录。这三道守卫来自社区的 `dshmarket` 管理器,它每一条都是从一个 bug 学来的。
 **`pnpm add` 成功不等于装好了插件。** 运行前先给 manifest 拍快照,pnpm 失败时恢复,失败的 add 不会留下依赖。之后逐个裁决 pnpm 加进来的包:既不声明组合包也不声明插件模块的,或者行 id 已被已组合层占有的组合包(对当前各层加候选层跑 `claimLayerIds`),再以 `pnpm remove` 移除并连同原因报在 `removed` 里;探针拒绝的包保留,因为视图能解释它、`retry` 还能再试。管理器一次只跑一个变更,第二个以 `plugins/busy` 拒绝而不是排队——否则每次写入都会在 manifest、用户层或 `node_modules` 上竞争——`add` 与 `uninstall` 在任一 agent 运行时以 `plugins/agents-running` 拒绝,因为 pnpm 会重写那些会话正在 import 的目录。这三道守卫来自社区的 `dshmarket` 管理器,它每一条都是从一个 bug 学来的。
 
 

+ 2 - 2
docs/event-producer-consumer.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
 #   pnpm run verify-translation-pairing --write docs/event-producer-consumer.md
-event-producer-consumer.md: aa7358ccfd003c6b18b29368a7dc8f57c184948d
-event-producer-consumer.zh.md: 3d1afbd05d547941cc5a6dc0305ccf698022c9a1
+event-producer-consumer.md: dcc376c2e49eb235c60484fee20f608cedc61d0d
+event-producer-consumer.zh.md: 03dcad8ca08dfdc1cd82c720ea6b712c8a54ee59

+ 2 - 2
docs/event-producer-consumer.md

@@ -46,8 +46,8 @@ This matrix shows which packages dispatch each harness-owned event and which pac
 | `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
 | `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
 | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
 | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
 | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:71`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
 | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:71`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
-| `plugins/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:239`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
-| `plugins/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:245`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugins/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:244`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugins/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:250`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
 | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
 | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
 | `session/created` | `emit` | [`packages/core/session/src/index.ts:50`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/created` | `emit` | [`packages/core/session/src/index.ts:50`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:60`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
 | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:60`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |

+ 2 - 2
docs/event-producer-consumer.zh.md

@@ -48,8 +48,8 @@
 | `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
 | `goal/changed` | `emit` | [`packages/goal/goal/src/domain.ts:114`](../packages/goal/goal/src/domain.ts) | [`goal`](../packages/goal/goal) (`emit`) | [`goal-round-driver`](../packages/goal/goal-round-driver) |
 | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
 | `llm/adapters-updated` | `emit` | [`packages/llm/llm/src/types.ts:23`](../packages/llm/llm/src/types.ts) | [`llm`](../packages/llm/llm) (`events.dispatch`) | [`acp`](../packages/acp/acp), [`llm`](../packages/llm/llm), `remotes` |
 | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:71`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
 | `llm/stream` | `waterfall` | [`packages/llm/llm/src/index.ts:71`](../packages/llm/llm/src/index.ts) | [`llm`](../packages/llm/llm) (`waterfall`) | [`agent-loop`](../packages/core/agent-loop), [`llm`](../packages/llm/llm), [`llm-replay`](../packages/test-support/llm-replay), [`session-checkpoint-policy`](../packages/session/session-checkpoint-policy), [`session-title`](../packages/session/session-title) |
-| `plugins/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:239`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
-| `plugins/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:245`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugins/changed` | `emit` | [`packages/boot/plugin-manager/src/types.ts:244`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
+| `plugins/install-log` | `emit` | [`packages/boot/plugin-manager/src/types.ts:250`](../packages/boot/plugin-manager/src/types.ts) | [`plugin-manager`](../packages/boot/plugin-manager) (`emit`) | `remotes` |
 | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
 | `session-telemetry/record` | `waterfall` | [`packages/session/session-telemetry/src/index.ts:43`](../packages/session/session-telemetry/src/index.ts) | [`session-telemetry`](../packages/session/session-telemetry) (`waterfall`) | - |
 | `session/created` | `emit` | [`packages/core/session/src/index.ts:50`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/created` | `emit` | [`packages/core/session/src/index.ts:50`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`compaction`](../packages/compaction/compaction), [`goal`](../packages/goal/goal), [`hook-protocol`](../packages/hooks/hook-protocol), [`llm-retry`](../packages/llm/llm-retry), [`permission-presets`](../packages/interaction/permission-presets), [`plan-mode`](../packages/plan/plan-mode), [`schedule`](../packages/schedule/schedule), `server`, [`session`](../packages/core/session), `session-controller`, [`session-log-deepseek`](../packages/session/session-log-deepseek), [`session-projection`](../packages/session/session-projection), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title), [`time-context`](../packages/context/time-context), [`tool-todo`](../packages/todo/tool-todo), [`tool-workflow`](../packages/workflow/tool-workflow), [`tools`](../packages/core/tools), [`user-approval`](../packages/interaction/user-approval) |
 | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:60`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |
 | `session/disposed` | `emit` | [`packages/core/session/src/index.ts:60`](../packages/core/session/src/index.ts) | [`session`](../packages/core/session) (`events.dispatch`) | [`agent-loop`](../packages/core/agent-loop), `agent-team`, `file-upload`, `session-controller`, [`session-persistence-jsonl`](../packages/session/session-persistence-jsonl), [`session-projection-cache`](../packages/session/session-projection-cache), [`session-telemetry`](../packages/session/session-telemetry), [`session-title`](../packages/session/session-title) |

+ 2 - 2
packages/boot/plugin-manager/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # 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:
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write packages/boot/plugin-manager/README.md
 #   pnpm run verify-translation-pairing --write packages/boot/plugin-manager/README.md
-README.md: 3e9774a6010317be0766ecafbb2686671070af1a
-README.zh.md: 4b0ef14243298cf1bd17a6277652a4c6e573bbec
+README.md: 1ce479a1bc65813e7625bca722d28e43d0ae0fb9
+README.zh.md: 79f9394aa9610b4ee664a2a11da38fb17d7f40d5

+ 1 - 1
packages/boot/plugin-manager/README.md

@@ -78,7 +78,7 @@ console.log(await manager.list())
 
 
 `addRow` inserts a row naming one of the package's modules — its main export for a `plugin` package, or a `dsh.plugins` entry — into the profile's global `cordis.patch.yml` (`target: { kind: 'global' }`) or an agent preset's user layer (`{ kind: 'preset', preset }`, through the roster's `overlayPathFor`). The row id derives from the package name and subpath unless given; a taken id fails with `plugins/row-conflict`. `removeRow` removes an inserted row and `setRowDisabled` writes or removes a `disabled: true` for any row — deny-only, so a bundle's own `!!js` gate is restored rather than overridden. The global layer is recomposed live on the spot; a preset's layer reaches its next standing generation. `dependents` says what disabling or removing a package would strand: services its rows provide that rows outside it inject, and user-layer rows naming its modules.
 `addRow` inserts a row naming one of the package's modules — its main export for a `plugin` package, or a `dsh.plugins` entry — into the profile's global `cordis.patch.yml` (`target: { kind: 'global' }`) or an agent preset's user layer (`{ kind: 'preset', preset }`, through the roster's `overlayPathFor`). The row id derives from the package name and subpath unless given; a taken id fails with `plugins/row-conflict`. `removeRow` removes an inserted row and `setRowDisabled` writes or removes a `disabled: true` for any row — deny-only, so a bundle's own `!!js` gate is restored rather than overridden. The global layer is recomposed live on the spot; a preset's layer reaches its next standing generation. `dependents` says what disabling or removing a package would strand: services its rows provide that rows outside it inject, and user-layer rows naming its modules.
 
 
-The manager runs one mutation at a time — a second call while one runs fails with `plugins/busy` naming the operation in flight — and `add` and `uninstall` refuse to change `node_modules` while a session is running, with `plugins/agents-running`. Every change is followed by a `plugins/changed` event on the context, and an install run emits pnpm's output as `plugins/install-log` chunks, each naming the command line it ran and, with colour on, carrying pnpm's SGR escapes.
+The manager runs one mutation at a time — a second call while one runs fails with `plugins/busy` naming the operation in flight — and `add` and `uninstall` refuse to change `node_modules` while a session is running, with `plugins/agents-running`. Every change is followed by a `plugins/changed` event on the context, and an install run emits pnpm's output as `plugins/install-log` chunks, each naming the command line it ran and the profile directory it ran in, and, with colour on, carrying pnpm's SGR escapes.
 
 
 ### Failures
 ### Failures
 
 

+ 1 - 1
packages/boot/plugin-manager/README.zh.md

@@ -78,7 +78,7 @@ console.log(await manager.list())
 
 
 `addRow` 把一条点名该包某个模块的行——`plugin` 包的主导出,或某个 `dsh.plugins` 条目——插入 profile 的全局 `cordis.patch.yml`(`target: { kind: 'global' }`)或某个 agent preset 的用户层(`{ kind: 'preset', preset }`,经 roster 的 `overlayPathFor`)。行 id 未给出时由包名与子路径派生;已被占用的 id 以 `plugins/row-conflict` 失败。`removeRow` 移除一条插入的行,`setRowDisabled` 为任意行写入或移除 `disabled: true`——只写拒绝,因此组合包自己的 `!!js` 门被恢复而不是被覆盖。全局层当场在线重新组合;preset 的层在其下一个常驻代际生效。`dependents` 说明停用或移除一个包会搁浅什么:其行提供而包外的行注入的服务,以及点名其模块的用户层行。
 `addRow` 把一条点名该包某个模块的行——`plugin` 包的主导出,或某个 `dsh.plugins` 条目——插入 profile 的全局 `cordis.patch.yml`(`target: { kind: 'global' }`)或某个 agent preset 的用户层(`{ kind: 'preset', preset }`,经 roster 的 `overlayPathFor`)。行 id 未给出时由包名与子路径派生;已被占用的 id 以 `plugins/row-conflict` 失败。`removeRow` 移除一条插入的行,`setRowDisabled` 为任意行写入或移除 `disabled: true`——只写拒绝,因此组合包自己的 `!!js` 门被恢复而不是被覆盖。全局层当场在线重新组合;preset 的层在其下一个常驻代际生效。`dependents` 说明停用或移除一个包会搁浅什么:其行提供而包外的行注入的服务,以及点名其模块的用户层行。
 
 
-管理器一次只跑一个变更——上一个还在跑时再调用会以 `plugins/busy` 失败并点名正在进行的操作——`add` 与 `uninstall` 在有会话运行时拒绝改动 `node_modules`,报 `plugins/agents-running`。每次变更之后在上下文上发出 `plugins/changed` 事件,安装运行把 pnpm 的输出以 `plugins/install-log` 分块发出,每块都写明所跑的命令行,开了颜色时还带着 pnpm 的 SGR 转义。
+管理器一次只跑一个变更——上一个还在跑时再调用会以 `plugins/busy` 失败并点名正在进行的操作——`add` 与 `uninstall` 在有会话运行时拒绝改动 `node_modules`,报 `plugins/agents-running`。每次变更之后在上下文上发出 `plugins/changed` 事件,安装运行把 pnpm 的输出以 `plugins/install-log` 分块发出,每块都写明所跑的命令行与所在的 profile 目录,开了颜色时还带着 pnpm 的 SGR 转义。
 
 
 ### 失败
 ### 失败
 
 

+ 3 - 3
packages/boot/plugin-manager/src/installer.ts

@@ -262,7 +262,7 @@ export class PluginInstaller {
       while (tailBytes > config.installLogTailBytes && tail.length > 1) {
       while (tailBytes > config.installLogTailBytes && tail.length > 1) {
         tailBytes -= Buffer.byteLength(tail.shift() as string)
         tailBytes -= Buffer.byteLength(tail.shift() as string)
       }
       }
-      this.options.installLog({ jobId, argv, spec, stream, text })
+      this.options.installLog({ jobId, argv, cwd: profileDir, spec, stream, text })
     }
     }
     // Windows resolves pnpm through its .cmd shim, which spawn() refuses
     // Windows resolves pnpm through its .cmd shim, which spawn() refuses
     // without a shell since the CVE-2024-27980 hardening. The parent
     // without a shell since the CVE-2024-27980 hardening. The parent
@@ -287,10 +287,10 @@ export class PluginInstaller {
     ).catch((error: unknown) => {
     ).catch((error: unknown) => {
       const message = messageOf(error)
       const message = messageOf(error)
       record('stderr', `${message}\n`)
       record('stderr', `${message}\n`)
-      this.options.installLog({ jobId, argv, spec, stream: 'stderr', text: '', exitCode: null })
+      this.options.installLog({ jobId, argv, cwd: profileDir, spec, stream: 'stderr', text: '', exitCode: null })
       throw new PluginOperationError('plugins/install-failed', `${NAME}: ${message}`, { spec, exitCode: null, log: tail.join('') }, { cause: error })
       throw new PluginOperationError('plugins/install-failed', `${NAME}: ${message}`, { spec, exitCode: null, log: tail.join('') }, { cause: error })
     })
     })
-    this.options.installLog({ jobId, argv, spec, stream: 'stdout', text: '', exitCode })
+    this.options.installLog({ jobId, argv, cwd: profileDir, spec, stream: 'stdout', text: '', exitCode })
     if (exitCode !== 0) {
     if (exitCode !== 0) {
       throw new PluginOperationError(
       throw new PluginOperationError(
         'plugins/install-failed',
         'plugins/install-failed',

+ 2 - 0
packages/boot/plugin-manager/src/types.ts

@@ -190,6 +190,8 @@ export interface PluginInstallLogChunk {
   readonly jobId: string
   readonly jobId: string
   /** The command line the run executes: pnpm's command name, then its arguments. */
   /** The command line the run executes: pnpm's command name, then its arguments. */
   readonly argv: readonly string[]
   readonly argv: readonly string[]
+  /** The directory the run executes in: the profile directory. */
+  readonly cwd: string
   /** The package spec the run installs or removes. */
   /** The package spec the run installs or removes. */
   readonly spec: string
   readonly spec: string
   readonly stream: 'stdout' | 'stderr'
   readonly stream: 'stdout' | 'stderr'

+ 3 - 3
packages/boot/plugin-manager/tests/plugin-manager.spec.ts

@@ -599,9 +599,9 @@ describe('PluginManager', () => {
       // The fake pnpm records the spec itself as the dependency name, which
       // The fake pnpm records the spec itself as the dependency name, which
       // resolves to nothing: a plain dependency whose probe cannot run.
       // resolves to nothing: a plain dependency whose probe cannot run.
       expect(result).toEqual({ installed: ['github:acme/ext-new'], removed: [], enabled: [], installedOnly: [], plain: ['github:acme/ext-new'], jobId: expect.any(String) as string })
       expect(result).toEqual({ installed: ['github:acme/ext-new'], removed: [], enabled: [], installedOnly: [], plain: ['github:acme/ext-new'], jobId: expect.any(String) as string })
-      expect(log.map(chunk => [chunk.argv, chunk.stream, chunk.text, chunk.exitCode])).toEqual([
-        [['pnpm', 'add', 'github:acme/ext-new'], 'stdout', '+ github:acme/ext-new 1.0.0\n', undefined],
-        [['pnpm', 'add', 'github:acme/ext-new'], 'stdout', '', 0],
+      expect(log.map(chunk => [chunk.argv, chunk.cwd, chunk.stream, chunk.text, chunk.exitCode])).toEqual([
+        [['pnpm', 'add', 'github:acme/ext-new'], staged.profileDir, 'stdout', '+ github:acme/ext-new 1.0.0\n', undefined],
+        [['pnpm', 'add', 'github:acme/ext-new'], staged.profileDir, 'stdout', '', 0],
       ])
       ])
       expect(changes).toEqual([{ reason: 'install' }])
       expect(changes).toEqual([{ reason: 'install' }])
     })
     })

+ 1 - 1
packages/extensions/tool-cordis/src/api-catalog.ts

@@ -4805,7 +4805,7 @@ export const TYPE_API: readonly TypeApiEntry[] = [
   },
   },
   {
   {
     name: 'PluginInstallLogChunk',
     name: 'PluginInstallLogChunk',
-    declaration: 'export interface PluginInstallLogChunk {\n    readonly jobId: string;\n    readonly spec: string;\n    readonly stream: \'stdout\' | \'stderr\';\n    readonly text: string;\n    readonly exitCode?: number | null;\n}',
+    declaration: 'export interface PluginInstallLogChunk {\n    readonly jobId: string;\n    readonly argv: readonly string[];\n    readonly cwd: string;\n    readonly spec: string;\n    readonly stream: \'stdout\' | \'stderr\';\n    readonly text: string;\n    readonly exitCode?: number | null;\n}',
   },
   },
   {
   {
     name: 'PluginInstallRejection',
     name: 'PluginInstallRejection',