dsh-model-switch 0.4.24 → 0.4.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## v0.4.25
2
+
3
+ Three coordinated changes: Provider groups now cover all four per-session capabilities (compaction, auto review, session naming, and subagents) with mid-session provider-switch following; capability switches stay disabled until the model catalog has loaded (and while any row save is in flight) so clicks are never silently dropped; and send-time switch compaction (`compactOnSwitch` / `src/switch-compact.ts`) has been removed completely.
4
+
5
+ - **Provider groups cover compaction, auto review, session naming, and subagents per session.**
6
+ - **Compaction (`src/compaction.ts`):** `mountEngine` mounts the official `BasicCompactionEngine` under `compactionScope(ctx)` (`ctx.extend({ llm: compactionLlm(...) })`). When `options.purpose === 'compaction'` and `options.sessionId` resolves to a session in `ctx.sessions`, `compactionLlm` derives the session's provider (`pending` selection preferred over `lastUsed`, then logged routes), resolves **provider group → flat compaction card → engine default**, and updates `options.provider` / `options.model` in place before delegating to `real.stream(options)`. Because `summarizeWithLlm` returns `{ provider: options.provider, model: options.model }`, the durable `compaction/summary` record always matches the exact model that wrote the summary, and edits to `providerGroups` apply to the very next compaction with zero engine remounts.
7
+ - **Auto review (`src/auto-review.ts`):** Immediately after running `official.apply(scope)`, `bindAutoReviewSessionCorrelation` registers a `{ prepend: true }` `tools/pre-execute` listener on the review scope — which Cordis places ahead of the official gate's own prepended listener in the waterfall — and binds `exec.agent?.session` in an `AsyncLocalStorage` scope around `next()`. When `classifyRisk` calls `scope.llm.stream(options)`, the wrapper reads the active session, resolves **provider group → flat reviewer card → session model**, and passes a shallow copy of the frozen options with the resolved route. Parallel sessions on different providers therefore review on their own configured models without cross-session interference, and mid-session provider switches take effect on the very next review.
8
+ - **Provider groups UI (`src/client/ModelSwitchSettings.tsx`):** Restored the **压缩模型 (`compaction`)** and **自动审查模型 (`autoReview`)** rows alongside **会话命名模型 (`sessionTitle`)** and **子代理默认路由 (`subagent`)** in the Provider groups card, seeded unsaved groups from all four active cards, and automatically reactivated stored `compactProvider`/`compactModel` and `autoReviewProvider`/`autoReviewModel` entries from v0.4.22/23 without migration.
9
+ - **Fixed intermittent unresponsive capability switches (`src/client/ModelSwitchSettings.tsx`).** Previously, clicking a capability switch (such as Session naming) before the async model catalog finished loading — or while another card's save was in flight — hit a silent `return` in `persist*Enabled` / `run()` while the `InstantSwitch` itself rendered enabled. Every switch's `disabled` prop now reflects `busy !== undefined`, `!writable`, `value === undefined`, and `catalogSeed === undefined` when enabling a seed-dependent route.
10
+ - **Removed send-time switch compaction (`compactOnSwitch` / `src/switch-compact.ts`).** Removed the Send protection (`发送保护` / `切换时压缩`) setting, UI section, and `agent/pre-step` hook. All compaction policy and triggers now belong exclusively to DSH's official compaction engine (`/compact` and automatic pressure/overflow compaction).
11
+
1
12
  ## v0.4.24
2
13
 
3
14
  Three coordinated changes: session naming gains a configurable model; provider groups (and the Default Subagent route) become per-session derived policy instead of a write, fixing sessions on different providers overwriting or redirecting each other's auxiliary routes; and every settings row save becomes one atomic revision-fenced mutation, fixing the save-while-running freeze, half-written provider/model pairs, and the ambiguous ⚠ marker.
package/PRODUCT.md CHANGED
@@ -11,6 +11,8 @@
11
11
  - **Image Router:** owns a new, uniquely named stable `generate_image` tool. It selects the configured Codex or Grok adapter/model at execution time, rejects backend-incompatible fields, and returns normalized image metadata. Existing provider-specific image tools remain available as rollback paths.
12
12
  - **Composer Picker:** replaces the composer model seat with a suffix-grouped Model / Effort / Context / Fast / Thinking picker and submits through the official Model Directory. Variant changes preserve an effort only when the target catalog row supports it, otherwise they use the target default or omit effort. Picker changes are session-local. A submitted prompt reserves the selected runtime immediately (composer submitting, pending first turn, or running). Native Antigravity keeps its models and effort; DSH keeps LLM-provider choices and disables Agent providers. After a successful native binding, other groups stay disabled. Cancel without a binding releases the reservation; existing DSH history still cannot convert onto an External Agent. The deployment default write a Host performs alongside a Session switch is left to DSH (since 0.4.23 the plugin no longer restores a pre-switch default).
13
13
  - **Plan Review:** intercepts the official `plan-review` composer takeover with a header execution-model picker and right-aligned Discuss in chat / Reject / Set as goal and run / Confirm actions. Confirm commits the selected model before answering; a refused commit leaves the review pending and retryable. Reject carries optional human feedback as the answer's own `custom` text, which plan mode reports to the model. **Set as goal and run** arms this Plan as the session's goal through the released `remote.goals.create` — replacing an unfinished goal with `get` + `clear` first so the new Plan receives a fresh round cap — and only then answers, so `dsh-goal-round-driver` carries the Plan after plan mode exits. The goal is set before the answer, a refusal keeps the review pending and retryable, and a deployment that mounts no goal service disables the action with an explicit reason; the plugin stores no goal state, schedules no rounds, and owns no part of the goal domain. The card body carries only the official two-part summary (first line, first paragraph); the full plan opens in the right sidebar through ui-plan's registered preview opener, which this card drives because a composer-takeover entry cannot declare the official actions slot. Discuss in chat withdraws the takeover through the Host's own verb — `dismiss()` where the Host publishes it, `cancel()` on the older generation — and names the Host when neither seam exists, so a settled review is never a dead end. The same runtime reservation applies.
14
+ - **Auto review:** the reviewer model for the official `@deepseek-ai/dsh-experimental-auto-review` per-call gate, off by default — both fields empty means reviews follow the session's own model. On pins a deployment-wide provider/model, and each review resolves its route per session: the session's provider group's reviewer pair → the flat card pair → the session's own model. Session correlation rides an outer `{ prepend: true }` `tools/pre-execute` listener on the review scope (`exec.agent.session`), so parallel sessions on different providers never interfere and mid-session provider switches take effect on the very next review.
15
+ - **Compaction:** the summarization model behind `/compact` and DSH's automatic compaction, off by default — both fields empty means compaction follows the conversation target model. On pins a deployment-wide provider/model, and each summarization call resolves its route per session: the session's provider group's compaction pair → the flat card pair → the official row's `modelPolicies` / conversation-target default. When a per-session route applies, the child context's `llm` wrapper updates `options.provider`/`options.model` in place on `purpose === 'compaction'` calls so `summarizeWithLlm` returns and durably records the exact route that wrote the summary in `compaction/summary`.
14
16
  - **Session naming:** the model behind DSH's automatic session titles, off by default — both fields empty means naming follows the session's own model, which is the official behavior. On pins a deployment-wide provider/model that each session re-resolves per request: provider group → flat pair → the session's own model. The route is read through on every naming request, so a save applies to the very next request with no restart and never interrupts an in-flight request; the exact pair used is recorded in the session's own `session/title-llm-request` event by the official naming implementation, so the record and the request always agree. The plugin replaces the official `session-title-llm` row (one registered naming provider per context) and mirrors the cadence that row declared; naming cadence and title policy remain DSH's.
15
17
 
16
18
  Model choices are capability-driven. Invalid, unavailable, or unsupported selections remain visible to configuration surfaces and fail loudly at use; routing never silently falls back.
@@ -21,11 +23,11 @@ Official `agent-presets` stay in the process. A nested `ctx.plugin(AgentPresets)
21
23
 
22
24
  ## Settings and lifecycle
23
25
 
24
- The settings surface aggregates public Settings namespaces, reads and mutates with optimistic revisions, and saves rows independently. Changing a fixed Subagent model applies only the target catalog row's default effort, so an effort never leaks across providers or models. The Subagent card is a RouteCard with an instant on/off for the Default Subagent route; turning it off keeps the last provider/model on disk. Switch compaction is a B-row in the Send protection group (发送保护), default on, written immediately. The Auto review card is a RouteCard with an instant switch defaulting **off = reviews follow the session model**; turning it on pins the catalog head as an editable starting point. The Compaction card mirrors it: default **off = official default = compaction follows the conversation target model**; turning it on pins the catalog head as an editable starting point, and the pinned pair drives `/compact` and official automatic compaction alike. The Session naming card mirrors them again: default **off = official default = naming follows the session's own model**, and every naming request re-reads the resolved route. The settings page hosts six route cards — Subagent, Web search, Image generation, Auto review, Compaction, and Session naming — plus the Provider groups card. Provider groups store one session-naming/subagent route group per provider in the shared `model-switch` namespace as a DERIVED policy, never a write: each session and each request resolves those two routes from its own provider — group pair → flat deployment-wide pair → official default — so switching a session's provider writes nothing and two sessions on different providers can no longer overwrite or redirect each other's auxiliary routes; the group editor covers exactly those two routes, auto review and compaction stay deployment-wide cards, and the stored format is unchanged (an older entry's compaction/auto-review fields are ignored; no migration). Every row save is one atomic, revision-fenced official ConfigForm mutation: a refused write leaves no half-written provider/model pair, an edited draft survives external snapshot updates, a conflict is reported with a reload path, a write that never settles is reported after 30 seconds instead of freezing the page, and the ⚠ marker means only "this value is not in the current provider catalog". There is no Main-default editing surface: the Host persists the deployment default from the last session switch, and a fixed default belongs to DSH's own Models page. Search and image routes are out of scope for groups. Every registration, watcher, and dynamic tool generation has clean disposal. All visible copy lives in zh/en locale dictionaries.
26
+ The settings surface aggregates public Settings namespaces, reads and mutates with optimistic revisions, and saves rows independently. Changing a fixed Subagent model applies only the target catalog row's default effort, so an effort never leaks across providers or models. The Subagent card is a RouteCard with an instant on/off for the Default Subagent route; turning it off keeps the last provider/model on disk. The Auto review, Compaction, and Session naming cards are RouteCards with instant switches defaulting **off = follow the session/conversation model**; turning a switch on pins the catalog head as an editable starting point, and the switch stays disabled until the async model catalog has loaded (or while any row save is in flight) so clicks are never silently dropped. The settings page hosts six route cards — Subagent, Web search, Image generation, Auto review, Compaction, and Session naming — plus the Provider groups card. Provider groups store one compaction / auto-review / session-naming / subagent route group per provider in the shared `model-switch` namespace as a DERIVED policy, never a write: each session and each request resolves those four routes from its own provider (`pending` selection preferred over `lastUsed`, then logged routes) — group pair → flat deployment-wide pair → official default — so switching a session's provider writes nothing, parallel sessions on different providers never overwrite each other, and mid-session provider switches take effect on the very next call. Every row save is one atomic, revision-fenced mutation through the official ConfigForm `mutate`.
25
27
 
26
28
  ## Exclusions
27
29
 
28
- Send-time context protection (check, and compact with the previous model when a sent message would exceed the target window) is in scope. The Auto reviewer route override rides a name-matched replacement of the experimental auto-review row and changes only the review request's provider/model. The compaction summarization route override rides a name-matched replacement of the official compaction-basic engine and changes only the summarization request's provider/model, forwarding the row's own engine config unchanged. Both auxiliary routes stay deployment-wide by design. The official compaction transaction durably records the provider/model that wrote the summary, so rewriting the summarization call per session would make that record false — the supported per-route alternative is the official exact-target `modelPolicies` array on the profile's `compaction-basic` row config (each entry matches one exact routed conversation target and overrides its compaction policy, including the summarization pair), which this plugin forwards verbatim. The official auto-review gate builds its `GenerateOptions` with no `sessionId`/`purpose` and closes over the single context it was applied with, so its interception point carries no session identity and a review cannot be attributed to a session. Both gaps, and the seams they would need, are recorded in `docs/seam-gap.md`. Because the Web surface runs session compaction inside each agent preset's isolated compaction group and profile patches cannot address rows inside a preset's plugins list, the plugin replaces the shipped preset rows wholesale (the Web preset editor's own save mechanism) to swap that group's engine; the copies are re-synced per DSH release during the existing compatibility verification, and a deployment that edits presets in the Web editor owns the merge from its own wholesale row. Model Switch does not own global compaction policy, titles, fetch routing, fallback, load balancing, provider credentials, CLI/API, configuration history, or DSH Core edits. Coordinated changes are limited to Model Switch and self-maintained provider plugins. Installing this package replaces a standalone `dsh-composer-picker` install; keep only one composer model seat.
30
+ Send-time switch compaction (`compactOnSwitch`) was removed in 0.4.25: injecting plugin-authored compaction checkpoints and notices during model switches interacted with the official `compaction-basic` step-pressure loop when a session's working set stayed near the target window's pressure threshold. All compaction policy and triggers belong exclusively to DSH's official compaction engine (`/compact` and automatic pressure/overflow compaction). The Auto reviewer route override rides a name-matched replacement of the experimental auto-review row and changes only the review request's provider/model. The compaction summarization route override rides a name-matched replacement of the official compaction-basic engine and changes only the summarization request's provider/model, forwarding the row's own engine config unchanged. Because the Web surface runs session compaction inside each agent preset's isolated compaction group and profile patches cannot address rows inside a preset's plugins list, the plugin replaces the shipped preset rows wholesale (the Web preset editor's own save mechanism) to swap that group's engine; the copies are re-synced per DSH release during the existing compatibility verification, and a deployment that edits presets in the Web editor owns the merge from its own wholesale row. Model Switch does not own global compaction policy, titles, fetch routing, fallback, load balancing, provider credentials, CLI/API, config migration, or DSH Core modifications.
29
31
 
30
32
  Official `settings.section` has no icon field. The Settings nav glyph is a label-matched DOM swap, the same published pattern as usage-monitor, until an official icon seam exists. The Plan Review takeover uses priority -5, and the composer model seat uses priority -10.
31
33
 
package/README.md CHANGED
@@ -90,19 +90,15 @@ The card body shows only the Plan's first-line title and first-paragraph summary
90
90
 
91
91
  ![Plan Review with an execution-model picker](docs/screenshots/plan-review.png)
92
92
 
93
- ## Send-time context protection
94
-
95
- When a sent message will use a different model or context-tier id than the last request, Model Switch checks the full pending request against the target window before that request is sent. Choosing a model in the picker does not compact. If the target is over budget, history is compacted once with the previous model through a private compaction engine; DSH's own automatic compaction settings are not changed. Progress appears as plugin notices in the transcript. The budget includes the completion notice and pending model-change notice; an oversized summary blocks the request. Failure keeps the user message in the session and does not send the target request or emit a switch-success notice. Turn the behavior off with **Settings → Model Switch → Check and compact context on send** (on by default). Native External Agent runtimes are not treated as DSH-compactable history.
96
-
97
93
  ## Auto review model
98
94
 
99
- The official `@deepseek-ai/dsh-experimental-auto-review` (the per-call reviewer for the Auto permission preset) hardwires its review requests onto the session's own route. This plugin takes that row over through a `cordis.patch.yml` row replacement: the official row is disabled and `dsh-model-switch/auto-review` mounts the **same official `apply`** under a scoped child context whose `llm.stream` `provider/model` are rewritten to the reviewer model pinned in Settings. The "Auto review model" card defaults to off = reviews follow the session model; turning it on pins the catalog head as an editable starting point, and saved changes apply to the very next review. When the optional auto-review bundle never started with the profile the row stays unmounted and the Auto preset keeps its official off state; an official row already active elsewhere is reported explicitly instead of producing a second `registerAuto`. The reviewer route is deliberately deployment-wide: the official gate builds its review request with no session identity and closes over the one context it was applied with, so no public seam can attribute a review to a session — the per-session route it would need is recorded in `docs/seam-gap.md`.
95
+ The official `@deepseek-ai/dsh-experimental-auto-review` (the per-call reviewer for the Auto permission preset) hardwires its review requests onto the session's own route. This plugin takes that row over through a `cordis.patch.yml` row replacement: the official row is disabled and `dsh-model-switch/auto-review` mounts the **same official `apply`** under a scoped child context whose `llm.stream` `provider/model` are rewritten to the reviewer route resolved for that session (provider group → flat card → session model). Session correlation rides a `{ prepend: true }` `tools/pre-execute` listener registered on the review scope immediately after `official.apply(scope)`, binding `exec.agent.session` in an `AsyncLocalStorage` scope around `next()` so parallel sessions on different providers never interfere and mid-session provider switches take effect on the very next review. The "Auto review model" card defaults to off = reviews follow the session model; turning it on pins the catalog head as an editable starting point, and saved changes apply to the very next review. When the optional auto-review bundle never started with the profile the row stays unmounted and the Auto preset keeps its official off state; an official row already active elsewhere is reported explicitly instead of producing a second `registerAuto`.
100
96
 
101
97
  ## Compaction model
102
98
 
103
99
  Official compaction — `/compact` and DSH's automatic compaction — summarizes history with the conversation's target model by default. The "Compaction model" card (default off) pins that summarization route instead: turning it on pins the catalog head as an editable starting point, and saved changes apply to the very next compaction. Leaving both fields empty returns to the official default. Saving while a compaction is running may interrupt it; it retries next time.
104
100
 
105
- The pair stays deployment-wide by design: the official compaction transaction durably records the provider/model that wrote the summary, so rewriting the summarization call per session would make that record false. For route-scoped summarization the supported alternative is the official exact-target `modelPolicies` array on the profile's `compaction-basic` row config: each entry matches one exact routed conversation target and overrides its compaction policy — including the summarization pair — for that target, and this plugin forwards the row's own config verbatim (the card's pair is overlaid only when both fields are set):
101
+ Each summarization call (`options.purpose === 'compaction'`) also resolves its route per session through `compactionScope`: the session's provider group's compaction pair → the flat card pair → the official row's `modelPolicies` / conversation-target default. When a route applies, the child context's `llm` wrapper updates `options.provider` and `options.model` in place before `ctx.llm.stream(options)`, so `summarizeWithLlm` returns and durably records the exact provider/model that wrote the summary in `compaction/summary`. Exact-target `modelPolicies` on the profile's `compaction-basic` row config are still forwarded verbatim:
106
102
 
107
103
  ```yaml
108
104
  - id: compaction-basic
@@ -115,7 +111,7 @@ The pair stays deployment-wide by design: the official compaction transaction du
115
111
  summarizationModel: deepseek-reasoner
116
112
  ```
117
113
 
118
- How the route reaches the engine depends on where the deployment runs its agent plane. The Web surface moves compaction into each agent preset's isolated `compaction` group, and profile patches cannot address rows inside a preset's `plugins` list — so this plugin replaces each shipped preset row wholesale (the same id-targeted mechanism the Web preset editor saves with) and swaps that group's official `compaction-basic` for `model-switch-compaction`, which mounts the **same official `BasicCompactionEngine`** with only `summarizationProvider`/`summarizationModel` overlaid (either field alone is ignored). Surfaces that compose the agent process-wide (the TUI) use the top-level row replacement instead. Two consequences: a DSH release that edits those presets needs the copies re-synced (this plugin's per-release verification covers it), and a deployment that edits presets in the Web editor writes its own wholesale row that then owns the merge.
114
+ How the route reaches the engine depends on where the deployment runs its agent plane. The Web surface moves compaction into each agent preset's isolated `compaction` group, and profile patches cannot address rows inside a preset's `plugins` list — so this plugin replaces each shipped preset row wholesale (the same id-targeted mechanism the Web preset editor saves with) and swaps that group's official `compaction-basic` for `model-switch-compaction`, which mounts the **same official `BasicCompactionEngine`** under `compactionScope`. Surfaces that compose the agent process-wide (the TUI) use the top-level row replacement instead. Two consequences: a DSH release that edits those presets needs the copies re-synced (this plugin's per-release verification covers it), and a deployment that edits presets in the Web editor writes its own wholesale row that then owns the merge.
119
115
 
120
116
  Known issue (upstream; recorded in `docs/seam-gap.md`): while the working set stays above the official engine's pressure threshold, automatic compaction can fire on every step and keep re-attempting ranges whose summaries fail the engine's own "summary must be smaller" guard — a visible loop of automatic compactions that this plugin does not control (verified in a session A/B with the plugin enabled and disabled). Mitigations on the forwarded row config: `auto: false` (keeps `/compact`), a higher `thresholdRatio`, a smaller mounted tool schema, or a pinned compaction model whose summaries are leaner.
121
117
 
@@ -127,11 +123,9 @@ The route is read through on every naming request: a saved change applies to the
127
123
 
128
124
  ## Provider groups
129
125
 
130
- The "Provider groups" card stores one route group per provider — the session-naming and subagent routes saved together — and a group saved for provider X applies to the sessions whose own provider is X. Groups are **derived, not written**: switching a session's provider writes nothing, so two sessions on different providers can no longer overwrite or redirect each other's auxiliary routes (before 0.4.24 a switch *wrote* the group into the shared fields, and one session's switch could steer another session's naming and subagent requests).
131
-
132
- Every request derives its own route from durable state: the session's provider (its model selection — a just-made pending selection counts — else its latest logged request route), then the group pair for that provider, then the flat deployment-wide pair, then the official default (the session's own model for naming; official inherit for subagents, where the fixed card applies only in `fixed` mode). Because nothing is written, this works for sessions this browser never opened and for switches made from the TUI or another client, with no watcher and no residual boundary; a session with no observable provider simply resolves to the deployment default. A switch to a provider without a group changes nothing, and a switch within one provider re-resolves to the same routes.
126
+ The "Provider groups" card stores one route group per provider — the compaction, auto-review, session-naming, and subagent routes saved together — and a group saved for provider X applies to the sessions whose own provider is X. Groups are **derived, not written**: switching a session's provider writes nothing, so two sessions on different providers can never overwrite or redirect each other's auxiliary routes.
133
127
 
134
- The group editor therefore covers exactly these two routes: auto review and compaction stay deployment-wide cards (see the two sections above for why), and search and image routes are out of scope. The stored configuration format is unchanged — an older group entry that still carries compaction or auto-review fields is accepted as-is and those fields are ignored, so no migration runs. In the editor, a provider's draft seeds from the currently derived routes the first time you configure it, each row's off state means "no pin — fall through to the card, then the official default", and **Delete group** removes the provider's entry entirely (clearing the stored field when the last group goes).
128
+ Every request derives its own route from durable state: the session's provider (its model selection — a just-made pending selection counts — else its latest logged request route), then the group pair for that provider, then the flat deployment-wide pair, then the official default (the conversation target for compaction; the session's own model for auto review and naming; official inherit for subagents, where the fixed card applies only in `fixed` mode). Because nothing is written, this works for sessions this browser never opened and for switches made from the TUI or another client, with no watcher and no residual boundary; a session with no observable provider simply resolves to the deployment default. Mid-session provider switches take effect on the very next compaction, review, title, or subagent call. In the editor, a provider's draft seeds from the currently active routes the first time you configure it, each row's off state means "no pin — fall through to the card, then the official default", and **Delete group** removes the provider's entry entirely (clearing the stored field when the last group goes).
135
129
 
136
130
  ## What Model Switch does not change
137
131
 
package/README.zh.md CHANGED
@@ -88,19 +88,15 @@ Plan Review 拥有独立于会话当前路由的执行模型草稿。**确认执
88
88
 
89
89
  ![带执行模型 Picker 的 Plan Review](docs/screenshots/plan-review.png)
90
90
 
91
- ## 发送时的上下文保护
92
-
93
- 当发送后的实际请求将使用与上次不同的模型或窗口档位时,Model Switch 会在目标请求发出前检查完整待发上下文。仅在选择器中选模型不会压缩。超出目标可用容量时,使用切换前的模型、通过私有压缩引擎压缩一次历史;不会改 DSH 自身的自动压缩设置。进度以插件通知出现在正文中。预算包含完成提示和待提交的模型切换提示;摘要仍超限时会阻止请求。失败时用户消息留在会话里,不会发送目标模型请求,也不会出现切换成功提示。可在 **设置 → 模型切换 → 发送时自动检查并压缩上下文** 关闭(默认开启)。原生 External Agent 运行时的内部上下文不会被当成 DSH 可压缩历史。
94
-
95
91
  ## 自动审查模型(Auto review)
96
92
 
97
- 官方 `@deepseek-ai/dsh-experimental-auto-review`(Auto 权限预设的逐调用审查)把审查请求固定在会话自身的路由上。本插件通过 `cordis.patch.yml` 的行替换接管这一行:官方行被禁用,改挂 `dsh-model-switch/auto-review`,它在作用域子上下文里运行**同一份官方 `apply`**,只把 `llm.stream` 的 `provider/model` 改写为设置里固定的审查模型。设置页「自动审查模型」默认关闭 = 审查跟随当前会话模型;打开时以目录首位模型作为可再改的固定起点,保存后对下一次审查立即生效。审查未配置官方能力包(`@deepseek-ai/dsh-experimental-auto-review` 未随 profile 启动)时本行不挂载,Auto 预设保持官方的关闭状态;官方行若已在别处注册,本行明确报错而不产生第二个 `registerAuto`。审查路由刻意保持部署级:官方闸门构造审查请求时不带会话身份、且只闭包它被 apply 的那一个上下文,因此没有任何公开接缝能把一次审查归属到某个会话——它需要的按会话路由已记录在 `docs/seam-gap.md`。
93
+ 官方 `@deepseek-ai/dsh-experimental-auto-review`(Auto 权限预设的逐调用审查)把审查请求固定在会话自身的路由上。本插件通过 `cordis.patch.yml` 的行替换接管这一行:官方行被禁用,改挂 `dsh-model-switch/auto-review`,它在作用域子上下文里运行**同一份官方 `apply`**,把 `llm.stream` 的 `provider/model` 改写为该会话解析出的审查模型(供应商配置组 → 全局审查卡片 → 会话自身模型)。在 `official.apply(scope)` 之后,本行立即在同一作用域注册 `{ prepend: true }` 的 `tools/pre-execute` 监听器(在 Cordis 瀑布流中排在官方监听器之前),用 `AsyncLocalStorage` 将 `exec.agent.session` 绑定到审查调用链上,因此不同供应商的多个会话并行时互不干扰,会话中途切换供应商也会在下一次审查立即生效。设置页「自动审查模型」默认关闭 = 审查跟随当前会话模型;打开时以目录首位模型作为可再改的固定起点,保存后对下一次审查立即生效。审查未配置官方能力包(`@deepseek-ai/dsh-experimental-auto-review` 未随 profile 启动)时本行不挂载,Auto 预设保持官方的关闭状态;官方行若已在别处注册,本行明确报错而不产生第二个 `registerAuto`。
98
94
 
99
95
  ## 压缩模型(Compaction model)
100
96
 
101
97
  官方压缩——`/compact` 与 DSH 的自动压缩——默认使用会话当前的目标模型做摘要。「压缩模型」卡片(默认关闭)改为固定摘要路由:打开时以目录首位模型作为可再改的固定起点,保存后对下一次压缩立即生效。两个字段都留空即回到官方默认。保存时若恰有压缩进行中,可能被打断并在下次重试。
102
98
 
103
- 摘要模型的固定对刻意保持部署级:官方压缩事务会持久记录写下摘要的 provider/model,若按会话改写摘要调用,这条持久记录就会与实际使用的路由不符。需要按路由区分摘要的部署,官方支持的做法是在 profile 的 `compaction-basic` 行配置里使用精确目标的 `modelPolicies` 数组:每个条目精确匹配一个实际路由的会话目标模型,并只对该目标覆盖其压缩策略——包括摘要路由;本插件会原样转发该行配置(仅当卡片两个字段都填写时才覆盖全局摘要对):
99
+ 每次摘要调用(`options.purpose === 'compaction'`)同样通过 `compactionScope` 按会话实时解析路由:该会话供应商组的压缩对 → 全局压缩卡片对 → 官方行的 `modelPolicies` / 会话目标模型默认。当解析出按会话路由时,子上下文的 `llm` 包装器在调用 `ctx.llm.stream(options)` 前原地更新 `options.provider` 与 `options.model`,从而让官方 `summarizeWithLlm` 返回并持久记录到 `compaction/summary` 的模型对与实际写摘要的模型完全一致。同时,profile 的 `compaction-basic` 行配置里的精确目标 `modelPolicies` 数组仍原样转发:
104
100
 
105
101
  ```yaml
106
102
  - id: compaction-basic
@@ -113,7 +109,7 @@ Plan Review 拥有独立于会话当前路由的执行模型草稿。**确认执
113
109
  summarizationModel: deepseek-reasoner
114
110
  ```
115
111
 
116
- 路由如何到达压缩引擎取决于部署在哪一层组合 agent。Web 面把压缩放进每个 Agent 预设的隔离 `compaction` 组里,而 profile patch 无法寻址 preset `plugins` 列表内部的行——因此本插件整表替换各预设行(与 Web 预设编辑器保存修改用的是同一套 id 定位机制),把该组的官方 `compaction-basic` 换成 `model-switch-compaction`,由它挂载**同一份官方 `BasicCompactionEngine`**、只覆盖 `summarizationProvider`/`summarizationModel`(只填其一忽略)。进程级组合 agent 的面(如 TUI)则走顶层行替换。两个后果:DSH 发布若改动这些预设,随包副本需要同步(本插件的逐版本核对覆盖这一步);部署若用 Web 编辑器改预设,会写出自己的整表行、并从那之后自行负责合并。
112
+ 路由如何到达压缩引擎取决于部署在哪一层组合 agent。Web 面把压缩放进每个 Agent 预设的隔离 `compaction` 组里,而 profile patch 无法寻址 preset `plugins` 列表内部的行——因此本插件整表替换各预设行(与 Web 预设编辑器保存修改用的是同一套 id 定位机制),把该组的官方 `compaction-basic` 换成 `model-switch-compaction`,由它在 `compactionScope` 下挂载**同一份官方 `BasicCompactionEngine`**。进程级组合 agent 的面(如 TUI)则走顶层行替换。两个后果:DSH 发布若改动这些预设,随包副本需要同步(本插件的逐版本核对覆盖这一步);部署若用 Web 编辑器改预设,会写出自己的整表行、并从那之后自行负责合并。
117
113
 
118
114
  已知问题(上游侧,见 `docs/seam-gap.md`):当工作集持续超过官方引擎的压力阈值时,自动压缩可能每步触发,并反复重试摘要过不了官方「摘要必须更小」守卫的小片段——表现为"不停自动压缩",本插件不控制该行为(已在同一会话做过插件开/关 A/B 对照)。可用的缓解(都在转发行配置上):`auto: false`(保留 `/compact`)、调高 `thresholdRatio`、减少挂载的工具 schema,或固定一个摘要更精炼的压缩模型。
119
115
 
@@ -125,11 +121,9 @@ DSH 会自动为新会话起名;命名的触发节奏由 profile 的官方命
125
121
 
126
122
  ## 供应商配置组(Provider groups)
127
123
 
128
- 「供应商配置组」卡片为每个供应商保存一组路由——会话命名与子代理两条路由一起存;为供应商 X 保存的组,作用于自身供应商就是 X 的会话。组是**推导出来的,而不是写进去的**:切换某个会话的供应商不写任何东西,因此不同供应商上的两个会话再也不能互相覆盖、互相改写对方的辅助路由(0.4.24 之前切换会把组**写进**共享字段,一个会话的切换能改道另一个会话的命名与子代理请求)。
129
-
130
- 每次请求都从持久状态推导自己的路由:先取该会话的供应商(它的模型选择——刚做出、尚未被请求消费的 pending 选择优先——其次最近一次已记录的请求路由),再取该供应商的组对,再取部署级的平铺对,最后是官方默认(命名用会话自身模型;子代理用官方继承,且平铺卡片仅在 `fixed` 模式下生效)。因为什么都不写,本浏览器从未打开过的会话、以及来自 TUI 或其他客户端的切换同样生效——没有 watcher,也没有剩余边界;观察不到供应商的会话直接落到部署默认。切换到没有配置组的供应商不改变任何东西,同一供应商内部切换推导出的还是同一些路由。
124
+ 「供应商配置组」卡片为每个供应商保存一组路由——压缩、自动审查、会话命名与子代理四条路由一起存;为供应商 X 保存的组,作用于自身供应商就是 X 的会话。组是**推导出来的,而不是写进去的**:切换某个会话的供应商不写任何东西,因此不同供应商上的多个会话并行时再也不会互相覆盖、互相改写对方的辅助路由。
131
125
 
132
- 因此组编辑器恰好只覆盖这两条路由:自动审查与压缩保持部署级卡片(原因见上面两节),搜索与图片路由不在范围内。存储格式不变——较早的组条目若还带着压缩/自动审查字段,会原样接受并忽略这些字段,无需迁移。编辑器里首次配置某供应商时,草稿从当前推导出的路由种子化;每行的关闭态表示"不固定——先落到卡片、再落到官方默认";「删除该组」彻底移除该供应商的条目(删掉最后一组时同时清掉存储字段)。
126
+ 每次请求都从持久状态推导自己的路由:先取该会话的供应商(它的模型选择——刚做出、尚未被请求消费的 pending 选择优先——其次最近一次已记录的请求路由),再取该供应商的组对,再取部署级的平铺对,最后是官方默认(压缩用会话目标模型;自动审查与命名用会话自身模型;子代理用官方继承,且平铺卡片仅在 `fixed` 模式下生效)。因为什么都不写,本浏览器从未打开过的会话、以及来自 TUI 或其他客户端的切换同样生效——没有 watcher,也没有剩余边界;观察不到供应商的会话直接落到部署默认。会话中途切换供应商后,下一次压缩、自动审查、命名或子代理调用都会立即跟随新供应商的配置组。编辑器里首次配置某供应商时,草稿从当前生效的路由种子化;每行的关闭态表示"不固定——先落到卡片、再落到官方默认";「删除该组」彻底移除该供应商的条目(删掉最后一组时同时清掉存储字段)。
133
127
 
134
128
  ## Model Switch 不会改变什么
135
129
 
@@ -1,3 +1,5 @@
1
+ import { n as sessionProviderOf, t as resolveCapabilityRoute } from "./session-route-BCQFzqRP.js";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
1
3
  //#region lib/types/auto-review.js
2
4
  /**
3
5
  * Model Switch-owned Auto reviewer row.
@@ -6,17 +8,20 @@
6
8
  * reviewer to the session's own request route (`header.config`). This row runs
7
9
  * the official `apply` under a scoped child context whose `llm` service is
8
10
  * shadowed by a wrapper that rewrites only `stream()`'s provider/model — and
9
- * only while the profile has a complete reviewer route configured. With no
10
- * route configured the wrapper passes options through by identity, so the
11
- * reviewer follows the session model exactly.
11
+ * only when the reviewing session's derived route (its provider group's
12
+ * reviewer pair, else the deployment-wide reviewer card) pins one. With no
13
+ * route pinned the wrapper passes options through by identity, so the reviewer
14
+ * follows the session model exactly.
12
15
  *
13
- * The reviewer route is deliberately DEPLOYMENT-WIDE. The official gate builds
14
- * its request with no session id and no purpose and closes over the single
15
- * context it was applied with, so this plugin has no public way to tell which
16
- * session a review serves; guessing one would attribute a review to the wrong
17
- * session and make the recorded reviewer route false. The unsupported
18
- * per-session reviewer route is recorded in `docs/seam-gap.md` with the public
19
- * seam it would need.
16
+ * Although the official gate omits `sessionId` and `purpose` from its frozen
17
+ * `GenerateOptions`, its `tools/pre-execute` waterfall listener receives
18
+ * `exec.agent` (`exec.agent.session`). After running `official.apply(scope)`,
19
+ * this module registers a `{ prepend: true }` `tools/pre-execute` listener on
20
+ * the same scope — which Cordis places ahead of the official gate's listener in
21
+ * the waterfall — and binds `exec.agent?.session` in an `AsyncLocalStorage`
22
+ * scope around `next()`. Every `scope.llm.stream()` call from `classifyRisk`
23
+ * therefore resolves the exact session being reviewed, with zero cross-session
24
+ * interference when multiple sessions review tool calls concurrently.
20
25
  *
21
26
  * The official gate stays an opt-in capability: the row mounts it only when
22
27
  * the profile actually started with the experimental-auto-review bundle.
@@ -78,12 +83,47 @@ function reviewLlm(real, route) {
78
83
  } });
79
84
  }
80
85
  /**
86
+ * Resolve the reviewer route for one session (or deployment-wide when no session is bound).
87
+ *
88
+ * @param ctx - host context whose modelSwitch settings and sessionProjections derive the route.
89
+ * @param session - the session whose tool call is being reviewed, when known.
90
+ * @returns The derived reviewer route, or undefined to follow the session model.
91
+ */
92
+ function resolveSessionAutoReviewRoute(ctx, session) {
93
+ let settings;
94
+ try {
95
+ settings = ctx.modelSwitch.currentSettings();
96
+ } catch {
97
+ settings = void 0;
98
+ }
99
+ const provider = sessionProviderOf(ctx, session);
100
+ return resolveCapabilityRoute(settings, provider, "autoReview");
101
+ }
102
+ /**
103
+ * Install the outer `tools/pre-execute` correlation listener ahead of the official gate.
104
+ *
105
+ * Because Cordis `on(..., { prepend: true })` unshifts onto the event hook list,
106
+ * registering this listener AFTER `official.apply(scope)` places it at index 0
107
+ * — directly ahead of the official gate's own prepended listener — so
108
+ * `activeSession.run(exec.agent?.session, next)` wraps the entire official
109
+ * review execution.
110
+ *
111
+ * @param scope - the review scope context where the gate is mounted.
112
+ * @param activeSession - AsyncLocalStorage holding the session under review.
113
+ */
114
+ function bindAutoReviewSessionCorrelation(scope, activeSession) {
115
+ scope.effect(function* () {
116
+ yield scope.on("tools/pre-execute", (exec, next) => activeSession.run(exec.agent?.session, next), { prepend: true });
117
+ }, "model-switch: auto-review session correlation");
118
+ }
119
+ /**
81
120
  * Build the scoped context the official gate runs under.
82
121
  * @param ctx - host context whose modelSwitch settings pin the reviewer route.
122
+ * @param activeSession - optional AsyncLocalStorage correlating each `tools/pre-execute` call to its session.
83
123
  * @returns A child context shadowing only the llm service.
84
124
  */
85
- function reviewScope(ctx) {
86
- return ctx.extend({ llm: reviewLlm(ctx.llm, () => autoReviewRouteOf(ctx.modelSwitch.currentSettings())) });
125
+ function reviewScope(ctx, activeSession = new AsyncLocalStorage()) {
126
+ return ctx.extend({ llm: reviewLlm(ctx.llm, () => resolveSessionAutoReviewRoute(ctx, activeSession.getStore())) });
87
127
  }
88
128
  /** Structural read of launcher-provided profile facts (the app-boot seam). */
89
129
  function autoReviewBundleActive(ctx) {
@@ -117,12 +157,15 @@ async function apply(ctx) {
117
157
  ctx.logger.error(cause);
118
158
  return;
119
159
  }
160
+ const activeSession = new AsyncLocalStorage();
161
+ const scope = reviewScope(ctx, activeSession);
120
162
  try {
121
- official.apply(reviewScope(ctx));
163
+ official.apply(scope);
164
+ bindAutoReviewSessionCorrelation(scope, activeSession);
122
165
  } catch (cause) {
123
166
  ctx.logger.error("model-switch: the official auto-review row is already active; the reviewer route override is unavailable");
124
167
  ctx.logger.error(cause);
125
168
  }
126
169
  }
127
170
  //#endregion
128
- export { apply, autoReviewRouteOf, inject, name, reviewLlm, reviewScope };
171
+ export { apply, autoReviewRouteOf, bindAutoReviewSessionCorrelation, inject, name, resolveSessionAutoReviewRoute, reviewLlm, reviewScope };