dsh-model-switch 0.4.30 → 0.4.32
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 +23 -0
- package/README.md +5 -3
- package/README.zh.md +5 -3
- package/lib/auto-review.js +480 -69
- package/lib/client.js +8 -2
- package/lib/types/auto-review-takeover.d.ts +100 -0
- package/lib/types/auto-review.d.ts +86 -22
- package/lib/types/client/locales.d.ts +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
## v0.4.32
|
|
2
|
+
|
|
3
|
+
A hot install could still break every tool call: replacing the plugin (disable → remove → install → enable, **without a DSH restart**) leaves the official `auto-review` row serving while this row's takeover is still trying to displace it, and its unshielded probe fails each call. This release makes that window non-fatal and shortens it.
|
|
4
|
+
|
|
5
|
+
- **P0: a broken reviewer seam no longer fails tool calls (`src/auto-review.ts`).** `installToolCallSafetyNet` registers a `{ prepend: true }` `tools/pre-execute` listener on this row's **own context**, so it is live even when the gate could not mount — the one window the defensive `permissionPresets` shadow cannot cover, because that shadow lives in the review scope while the serving official gate resolves the real service from the profile. It catches only seam failures (`inactive context`, `cannot get required service`, `cannot get property`, `without inject`, `TRANSPORT`), returns `{ kind: 'allow' }` so the body dispatches, and reports once with the fiber-chain dump. It is **inert while this row holds the route** (`isDegraded: () => !presets.registered`), so another plugin's broken listener keeps failing closed; reviewer transport/quota errors (`RATE_LIMIT`, `PI_AI_ERROR`) keep failing closed too — "the reviewer declined" and "the reviewer could not talk" are not broken seams. The waterfall only decides whether the body runs, so allowing here never masks a tool failure.
|
|
6
|
+
- **The takeover budget matches a real disposal (`src/auto-review-takeover.ts`).** 20×10 ms (~200 ms) → 60×50 ms (~3 s), plus `TAKEOVER_LATE_RETRY_DELAYS_MS` = 500/1000/1500 ms: three bounded re-claims after the loop gives up, because `Entry.update({ disabled: true })` starts the displaced fiber's teardown and the loader does not await it. A disposal that unmounts a gate, restores permission presets and unwinds fibers outlasted the old budget, which is exactly the reported hot-swap window. Timers are cleared with the row.
|
|
7
|
+
- **Actionable failure log.** The lost-seam error now says both what happens next ("the reviewer path is shielded instead of failing tool calls") and what the human can do ("Restart DSH to converge the reviewer row — a hot install cannot always displace the official row in time").
|
|
8
|
+
- **Docs.** `docs/seam-gap.md` records the two remaining gaps: no public seam lets a plugin **replace a bundled capability row at runtime** (only disable-and-reclaim through the loader), and the loader cannot address a row nested in a preset's `config.plugins` — the official bundle row is top-level today, but a future nesting would need a config-level bridge like 0.4.27's preset bridge.
|
|
9
|
+
- **Verification.** `pnpm run check` green (55 test files / 494 tests + pack gate). New coverage: the net allows the call and reports exactly once when the seam is broken, stays inert while this row holds the route, rethrows a reviewer quota error, and classifies an inject failure apart from a denial. The late-retry schedule is bounded by the row's lifetime. **Not yet reproduced end-to-end**: the disable → remove → install → enable sequence on the lab plane — this machine cannot reach the GitHub remote, so the release itself is still pending.
|
|
10
|
+
|
|
11
|
+
## v0.4.31
|
|
12
|
+
|
|
13
|
+
The recurring tool-call outage is closed at both ends it can be closed from: this plugin now **takes the Auto-reviewer seam over** from the official `auto-review` row (yield-then-claim, the session-title pattern), and its own gate's preset probe can no longer fail a tool call even while the preset service is broken — with a fiber-chain dump in the one degradation log so the upstream half of the failure names itself next time.
|
|
14
|
+
|
|
15
|
+
- **P0: the official `auto-review` row runs again and every tool call dies (`src/auto-review-takeover.ts`, `src/auto-review.ts`).** v0.4.30's defensive shadow protected only **this row's** gate; at 15:22:34 the plugin manager wrote `- id: auto-review / disabled: false` into the profile patch layer (it applies after every bundle layer, so this plugin's static `disabled` can never win), the official row re-activated beside this one, and its **unguarded** probe — `permissionPresets.current(session)` is the first `tools/pre-execute` step — turned every tool call into `cannot get required service "sessionProjections" in inactive context` until the plugin was disabled and the Host restarted. Two live rows are not survivable on that seam at all: `registerAuto` is single-registration, and whichever row loses it cannot keep serving. The row now displaces the official one through the loader's released seams (the same choreography as the session-title provider slot): on `loader/patch-context` it releases the Auto registration **synchronously before** `next()` — the loader runs the entry's plugin `apply` in the same turn, so an awaited yield would collide — lets the official row apply cleanly, disables it through `Entry.update({ disabled: true })`, and re-claims the registration with a bounded retry; `loader/partial-dispose` re-claims when a toggled-back-on official row goes away again, and an initial sweep covers rows composed before this one. Manual toggling of the official「自动审查」component is now a logged, self-healing no-op instead of an outage.
|
|
16
|
+
- **The Auto selection survives every displacement.** Disposing a gate moves live `auto` sessions to `danger-full-access` by design (the official teardown runs on both the displaced row and this row's own re-mounts), so every takeover step runs inside a preservation window that remembers the sessions on `auto` and restores them after the registration is live again.
|
|
17
|
+
- **P0: the preset probe can no longer fail a tool call, and the shadow is always on (`src/auto-review.ts`).** `defensivePermissionPresets` now resolves the presets service **lazily per call** (the shadow protects even while the service is still activating), degrades `current()` to `custom` so the gate skips review and runs the tool body, swallows a failing teardown `set()`, and reports the lost capability **once** — now including the exact resolution chain (`uid/state/name/inject` per fiber in the caller's chain). That dump matters because the upstream cause is real and recurring: `internal/get` walks the whole caller fiber chain and throws `… in inactive context` at the first fiber whose `inject` declares `sessionProjections` without a resolved snapshot — and **twenty-four** released rows declare it (`dsh-permission-presets` itself, `dsh-session-title`, `dsh-plan-mode`, `dsh-token-meter`, `dsh-tool-todo`, `dsh-agent-loop`, …), so any one of them mid-reload can break an unguarded probe. The upstream fix and the missing seams are recorded in `docs/seam-gap.md`.
|
|
18
|
+
- **`registerAuto` is brokered, not swallowed.** The gate's registration is held through `releaseForTakeover()` / `reclaimRegistration()` so the takeover can hand the single slot over and take it back **without unmounting the gate** (unmounting is what flips live sessions off Auto). A `registerAuto` conflict now fails the gate's mount loudly and the takeover retries, instead of leaving an Auto preset that silently does nothing — the behavior 0.4.30 shipped and which hid the first recurrence.
|
|
19
|
+
- **Honest correction to the v0.4.30 entry.** That entry's model was half right and half wrong, and the real-package harness settled it: the raw message does come from the official gate's unguarded probe (reproduced exactly), but the "half-applied effect leaks its listener" explanation is **false** — when `registerAuto` throws mid-effect, cordis cleans the already-yielded listener up (verified: zero probe calls after the failed mount). The outage needs the official gate actually **running**, which is why 0.4.30's shadow (covering only this row's gate) did not prevent it and why this release makes the official row unable to run at all.
|
|
20
|
+
- **Visibility.** The settings card's「自动审查」section now says that the official component is replaced by this plugin's configurable reviewer route and is re-disabled with a log line when toggled back on; `docs/seam-gap.md` gains the two missing seams (no supersede declaration for a bundled capability row + two layers of `disabled`; the caller-chain `sessionProjections` resolution), and READMEs point at the takeover behavior.
|
|
21
|
+
- **Verification.** `pnpm run check` green (55 test files / 490 tests + pack gate). New coverage: strict package identity (an id-only lookalike is never disabled), the displacement claims the registration around the window that preserves Auto sessions, the reclaim retries until the displaced registration releases and reports the lost seam otherwise, the patch-context yield is **synchronous before** the loader continues, repeated events join one reclaim per entry, the partial-dispose path re-claims, the sweep catches earlier-composed rows, and the lazy shadow degrades while the service is unresolvable and brokers yield/reclaim with the same admit callback. With the **real published packages** (official `auto-review` gate + cordis): the pre-fix shape's half-applied effect leaves **no** leaked listener (hypothesis disproven), the yield-then-claim shape lets the official row apply cleanly and its failure still fail-closes as a clean deny, and the shipped shadowed gate returns `allow` for a tool call with a broken preset service.
|
|
22
|
+
- **Residual (honest).** If the displaced registration never releases within the bounded retry, the takeover reports the lost seam and the official row keeps serving (unguarded) until the next loader cycle — no silent half-state, but also no reviewer route override. The upstream `sessionProjections` chain break can still fail **other** rows' own reads (anything else that injects it); this plugin cannot defend those, and the fiber-chain dump is the evidence-gathering step for the upstream report. Toggling the official component remains visible in the plugin manager as the user set it — the takeover's in-memory disable is re-applied at every startup rather than written over the user's patch.
|
|
23
|
+
|
|
1
24
|
## v0.4.30
|
|
2
25
|
|
|
3
26
|
One live failure class, three fixes: after a provider-group save every tool call in the session died before its body ran with `cannot get required service "sessionProjections" in inactive context`, the settings page sometimes answered a provider-group save with the raw `settings-rejected` sentinel, and the composer's permission selector sometimes disappeared.
|
package/README.md
CHANGED
|
@@ -92,7 +92,9 @@ The card body shows only the Plan's first-line title and first-paragraph summary
|
|
|
92
92
|
|
|
93
93
|
## Auto review model
|
|
94
94
|
|
|
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
|
|
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.
|
|
96
|
+
|
|
97
|
+
Since 0.4.31 the row also owns the seam at runtime, not just in the patch file. The plugin manager writes its component toggles into a layer that applies after every bundle layer, so a manual re-enable of the official「自动审查」component used to bring the official, unguarded gate back — and, because `permissionPresets.registerAuto` is single-registration, two live rows on that seam eventually killed every tool call. The row now takes the seam over the way the session-title slot is taken over: when the official row is about to apply it releases the Auto registration **synchronously first** (so the official row applies cleanly instead of colliding), disables the official row through the public `Entry.update`, and re-claims the registration with a bounded retry — preserving live sessions' Auto selection across the displacement. Toggling the official component back on is therefore a logged, self-healing no-op; the settings card's「自动审查」section says so too.
|
|
96
98
|
|
|
97
99
|
## Compaction model
|
|
98
100
|
|
|
@@ -123,7 +125,7 @@ The route is read through on every naming request: a saved change applies to the
|
|
|
123
125
|
|
|
124
126
|
Since 0.4.29 the row also coexists with the experimental "titles that update with the conversation" bundle (`dsh-experimental-session-titles-bundle`, which inserts an all-prompts naming row): enabling that bundle keeps this plugin owning the single naming-provider slot, mirrors the all-prompts cadence so titles refresh with every prompt, and keeps the naming-model and provider-group routes in force (the provider id recorded per title is `session-title-all-prompts-llm`, exactly what the official row would record). Disabling the bundle restores the first-prompt cadence with the same route override. The takeover only uses released loader seams — the `loader/patch-context` waterfall, `Entry.update`, and `sessionTitle.register` plus its disposer — letting the official row register successfully first and then disabling it, so enabling the bundle no longer raises the `already registered` activation-failure toast.
|
|
125
127
|
|
|
126
|
-
Since 0.4.30 the Auto review row also survives a degraded permission service. DSH's Auto gate reads `permissionPresets.current(session)` before every tool call, and when that service has gone inactive the read throws `cannot get required service "sessionProjections" in inactive context` outside the gate's own reviewer-error handling — so every tool call died before its body ran. The row
|
|
128
|
+
Since 0.4.30 the Auto review row also survives a degraded permission service. DSH's Auto gate reads `permissionPresets.current(session)` before every tool call, and when that service has gone inactive the read throws `cannot get required service "sessionProjections" in inactive context` outside the gate's own reviewer-error handling — so every tool call died before its body ran. The row shadows that service inside its review scope: a failed probe reports "not on Auto", the tool call runs normally, and one log line states that review was skipped. Since 0.4.31 the shadow is **always on** and resolves the service lazily per call (so it protects even while the service is still activating), and its one degradation line carries the exact fiber chain (`uid/state/name/inject` per fiber) that failed resolution — because the upstream cause is a loader lifecycle window: `internal/get` walks the whole caller fiber chain and twenty-four released rows declare `sessionProjections`, so any one of them mid-reload can break an unguarded probe (recorded in `docs/seam-gap.md` with the upstream proposal). The Auto registration itself is brokered — handed over and reclaimed around displacements without unmounting the gate, since unmounting is what flips live `auto` sessions to `danger-full-access` — and a gate re-mounted after a profile reload unloads its previous registration first, so the `auto` preset cannot disappear from the permission picker.
|
|
127
129
|
|
|
128
130
|
|
|
129
131
|
## Provider groups
|
|
@@ -181,7 +183,7 @@ Release-aligned search regression: `DSH_RELEASE_ANCHOR=/path/to/official/install
|
|
|
181
183
|
|
|
182
184
|
If `dsh-composer-picker` is installed, remove it from the profile before enabling Model Switch. Model Switch already owns the Composer Picker and Plan Review seat; two owners produce duplicate or competing UI.
|
|
183
185
|
|
|
184
|
-
Production profiles must use a released registry version or GitHub tag rather than workspace-local dependencies. Restart the selected DSH profile after installation or route changes.
|
|
186
|
+
Production profiles must use a released registry version or GitHub tag rather than workspace-local dependencies. Restart the selected DSH profile after installation or route changes. **Restart once after installing, updating, enabling or disabling this plugin**: it owns Host rows and takes the official Auto-review row over at runtime (`permissionPresets.registerAuto` is a single registration slot); when a hot enable skips the restart, the official row may still be serving, so the takeover only shields the reviewer path — tools keep running, that call's review is skipped with one log line — and the seam converges on the next start.
|
|
185
187
|
|
|
186
188
|
## Compatibility
|
|
187
189
|
|
package/README.zh.md
CHANGED
|
@@ -90,7 +90,9 @@ Plan Review 拥有独立于会话当前路由的执行模型草稿。**确认执
|
|
|
90
90
|
|
|
91
91
|
## 自动审查模型(Auto review)
|
|
92
92
|
|
|
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
|
|
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 预设保持官方的关闭状态。
|
|
94
|
+
|
|
95
|
+
0.4.31 起本行还在运行时接管这条缝,而不仅靠补丁文件:插件管理器会把组件开关写进"在所有 bundle 层之后生效"的层,手动把官方「自动审查」组件重新打开曾会让无防护的官方 gate 回来——而 `permissionPresets.registerAuto` 是单注册的,同一条缝上的两行最终杀掉每个工具调用。现在本行按接管会话命名槽位的同一手法接管:官方行将要 apply 时**先同步交出** Auto 注册(让官方行干净应用而不是撞车),再通过公开的 `Entry.update` 停用官方行,并以有界重试收回注册——整个位移过程保留会话的 Auto 选择。因此在插件管理器里把官方组件再打开只是一次会记日志、自愈的空操作;设置页「自动审查」卡片里也写明了这一点。
|
|
94
96
|
|
|
95
97
|
## 压缩模型(Compaction model)
|
|
96
98
|
|
|
@@ -121,7 +123,7 @@ DSH 会自动为新会话起名;命名的触发节奏由 profile 的官方命
|
|
|
121
123
|
|
|
122
124
|
0.4.29 起与实验包「随对话更新的标题」(`dsh-experimental-session-titles-bundle`,插入 all-prompts 命名行)兼容:启用该实验包时,本插件接管同一个命名 provider 槽位并镜像 all-prompts 节奏,标题随对话更新,同时命名模型路由与供应商组路由继续生效(`session/title` 记录中的 provider id 为 `session-title-all-prompts-llm`,与官方行一致);禁用实验包即回到首条提示节奏与同样的路由覆写。实现只使用 loader 已发布的缝(`loader/patch-context` 瀑布、`Entry.update`、`sessionTitle.register` 及其 disposer),让官方行先成功注册、再被停用并由本行重占槽位,因此启用时不再出现 `already registered` 激活失败弹窗。
|
|
123
125
|
|
|
124
|
-
0.4.30 起 Auto 审查行还能在许可服务降级时保住工具调用:DSH 的 Auto gate 在每次工具调用前读取 `permissionPresets.current(session)`,一旦该服务失活,这次读取会在 gate 自己的审查错误包装之外抛出 `cannot get required service "sessionProjections" in inactive context
|
|
126
|
+
0.4.30 起 Auto 审查行还能在许可服务降级时保住工具调用:DSH 的 Auto gate 在每次工具调用前读取 `permissionPresets.current(session)`,一旦该服务失活,这次读取会在 gate 自己的审查错误包装之外抛出 `cannot get required service "sessionProjections" in inactive context`,导致每个工具调用都在执行前失败。本行在审查作用域内遮蔽该服务:探测失败即按「不在 Auto」处理,工具调用照常执行,并只记一行日志说明审查被跳过。0.4.31 起该遮蔽**常开**且按调用惰性解析服务(服务尚未激活时也受保护),并把失败的解析链(每级 fiber 的 `uid/state/name/inject`)记进唯一那行降级日志——因为上游成因是 loader 生命周期窗口:`internal/get` 会沿调用方 fiber 链整链查找,而有 24 个已发布行在 `inject` 里声明了 `sessionProjections`,其中任何一行处于重载中都可能打断无防护的探测(连同上游建议一并记录在 `docs/seam-gap.md`)。Auto 注册本身改为**托管**:位移时同步交出、事后收回而不卸载 gate(卸载会把在用 `auto` 的会话翻成 `danger-full-access`);配置重载后重新挂载的 gate 会先卸载上一个注册,因此 `auto` 预设不会再从许可选择器里消失。
|
|
125
127
|
|
|
126
128
|
|
|
127
129
|
## 供应商配置组(Provider groups)
|
|
@@ -179,7 +181,7 @@ DeepSeek 薄适配器调用官方公开 `DeepSeekSearchProvider`,复用用户
|
|
|
179
181
|
|
|
180
182
|
如果 profile 已安装 `dsh-composer-picker`,请先移除它。Model Switch 已经拥有 Composer Picker 和 Plan Review 席位;同时安装会产生重复或竞争 UI。
|
|
181
183
|
|
|
182
|
-
生产 profile 必须使用已发布的注册表版本或 GitHub tag,不能使用工作区本地依赖。安装或修改路由后重启对应的 DSH profile
|
|
184
|
+
生产 profile 必须使用已发布的注册表版本或 GitHub tag,不能使用工作区本地依赖。安装或修改路由后重启对应的 DSH profile。**安装、更新、启用或禁用本插件后都要重启一次**:它持有 Host 侧的行,并在运行时接管官方的自动审查行(`permissionPresets.registerAuto` 只有一个注册位);不重启直接热启用时,官方行可能仍在服务,接管只能先把审查路径屏蔽掉——工具照常运行、本次审查跳过并留一条日志——重启后才会真正收敛。
|
|
183
185
|
|
|
184
186
|
## 兼容性
|
|
185
187
|
|
package/lib/auto-review.js
CHANGED
|
@@ -1,5 +1,206 @@
|
|
|
1
1
|
import { i as sessionProviderOf, r as resolveCapabilityRoute } from "./session-route-BjNPlliu.js";
|
|
2
2
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
/**
|
|
4
|
+
* The official package identity — the only handle on the row this plugin
|
|
5
|
+
* trusts. Matching by id alone could reach a group or another plugin's row, and
|
|
6
|
+
* a matched row is disabled (its fiber disposed).
|
|
7
|
+
*/
|
|
8
|
+
const OFFICIAL_AUTO_REVIEW_PACKAGE = "@deepseek-ai/dsh-experimental-auto-review";
|
|
9
|
+
/**
|
|
10
|
+
* Delays for the late re-claims after the attempt loop gave up (ms).
|
|
11
|
+
*
|
|
12
|
+
* `Entry.update({ disabled: true })` starts the displaced fiber's disposal but
|
|
13
|
+
* the loader does not await it, and a disposal that tears down a mounted gate,
|
|
14
|
+
* restores permission presets and unwinds fibers can take longer than the
|
|
15
|
+
* attempt loop's budget. These re-claims keep trying for a few more seconds so a
|
|
16
|
+
* hot-swapped install converges instead of needing a restart.
|
|
17
|
+
*/
|
|
18
|
+
const TAKEOVER_LATE_RETRY_DELAYS_MS = [
|
|
19
|
+
500,
|
|
20
|
+
1e3,
|
|
21
|
+
1500
|
|
22
|
+
];
|
|
23
|
+
/** Whether one loader entry is the official Auto-review row (strict package identity). */
|
|
24
|
+
function isOfficialAutoReviewRow(entry) {
|
|
25
|
+
if (entry === void 0) return false;
|
|
26
|
+
const pkg = entry.options?.name;
|
|
27
|
+
return typeof pkg === "string" && pkg === "@deepseek-ai/dsh-experimental-auto-review";
|
|
28
|
+
}
|
|
29
|
+
/** The first composed official Auto-review row, if the profile declares one. */
|
|
30
|
+
function officialAutoReviewEntry(loader) {
|
|
31
|
+
if (loader === void 0 || typeof loader.entries !== "function") return void 0;
|
|
32
|
+
for (const entry of loader.entries()) if (isOfficialAutoReviewRow(entry)) return entry;
|
|
33
|
+
}
|
|
34
|
+
/** The id one entry is known by (the loader's leaf id, not a nested path). */
|
|
35
|
+
function leafId(entry) {
|
|
36
|
+
const raw = entry.options?.id;
|
|
37
|
+
return typeof raw === "string" ? raw : void 0;
|
|
38
|
+
}
|
|
39
|
+
/** A loader entry the loader has not created a fiber for yet. */
|
|
40
|
+
function alreadyDisabled(entry) {
|
|
41
|
+
return entry.disabled === true;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Displace the official Auto-review row and take the registration back.
|
|
45
|
+
*
|
|
46
|
+
* The official row is disabled through the public `Entry.update` (which starts
|
|
47
|
+
* its fiber's disposal — the loader does not await it, so the claim retries
|
|
48
|
+
* until the displaced registration actually released). The Auto selection of
|
|
49
|
+
* live sessions is preserved around the displacement: the official gate's
|
|
50
|
+
* disposal moves `auto` sessions to `danger-full-access` by design, and this
|
|
51
|
+
* row's registration puts them back.
|
|
52
|
+
*
|
|
53
|
+
* @param ctx - this row's context, used only for logging.
|
|
54
|
+
* @param entry - the official row to displace.
|
|
55
|
+
* @param seam - the Auto-registration seam to yield and reclaim.
|
|
56
|
+
* @returns a promise settling once the takeover attempt loop finished.
|
|
57
|
+
*/
|
|
58
|
+
async function reclaimOfficialAutoReviewRow(ctx, entry, seam) {
|
|
59
|
+
const label = String(leafId(entry) ?? "auto-review");
|
|
60
|
+
/** One disable-then-claim step; true once this row holds the registration. */
|
|
61
|
+
const attempt = async () => {
|
|
62
|
+
try {
|
|
63
|
+
await entry.update({ disabled: true });
|
|
64
|
+
ctx.logger.info(`model-switch: disabled the official auto-review row ${label} (${OFFICIAL_AUTO_REVIEW_PACKAGE}); Model Switch's auto-review row owns the reviewer seam (configurable reviewer route)`);
|
|
65
|
+
} catch (cause) {
|
|
66
|
+
ctx.logger.info("model-switch: the displaced auto-review row could not be persisted as disabled; the takeover repeats next startup");
|
|
67
|
+
ctx.logger.info(String(cause));
|
|
68
|
+
}
|
|
69
|
+
return await seam.claim();
|
|
70
|
+
};
|
|
71
|
+
await seam.preserveAuto(async () => {
|
|
72
|
+
for (let index = 0; index < 60; index += 1) {
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, index === 0 ? 0 : 50));
|
|
74
|
+
if (await attempt()) {
|
|
75
|
+
ctx.logger.info(`model-switch: displaced the official auto-review row ${label}; Auto review runs under the configurable reviewer route`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
ctx.logger.error(`model-switch: the official auto-review row ${label} kept the Auto registration; the reviewer route override is unavailable this session`);
|
|
80
|
+
});
|
|
81
|
+
scheduleLateReclaims(ctx, label, seam);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Keep re-claiming for a few seconds after the attempt loop gave up.
|
|
85
|
+
*
|
|
86
|
+
* The displaced fiber's disposal starts on `Entry.update` but is not awaited by
|
|
87
|
+
* the loader, so it can outlast the attempt loop — which is exactly the hot-swap
|
|
88
|
+
* window where the official (unshielded) gate keeps serving. These bounded
|
|
89
|
+
* re-claims converge that window instead of leaving it to a restart. Timers are
|
|
90
|
+
* cleared when this row unloads.
|
|
91
|
+
*
|
|
92
|
+
* @param ctx - this row's context, used for logging and timer disposal.
|
|
93
|
+
* @param label - the official row's id, for diagnostics.
|
|
94
|
+
* @param seam - the Auto-registration seam to reclaim.
|
|
95
|
+
*/
|
|
96
|
+
function scheduleLateReclaims(ctx, label, seam) {
|
|
97
|
+
let stopped = false;
|
|
98
|
+
const timers = [];
|
|
99
|
+
const stop = () => {
|
|
100
|
+
stopped = true;
|
|
101
|
+
for (const timer of timers.splice(0)) clearTimeout(timer);
|
|
102
|
+
};
|
|
103
|
+
ctx.effect(function* () {
|
|
104
|
+
yield stop;
|
|
105
|
+
}, "model-switch: auto-review late takeover retries");
|
|
106
|
+
const run = (index) => {
|
|
107
|
+
if (stopped || index >= TAKEOVER_LATE_RETRY_DELAYS_MS.length) return;
|
|
108
|
+
const timer = setTimeout(() => {
|
|
109
|
+
(async () => {
|
|
110
|
+
if (stopped) return;
|
|
111
|
+
if (await seam.claim()) {
|
|
112
|
+
ctx.logger.info(`model-switch: reclaimed the Auto registration late from the official auto-review row ${label}; Auto review runs under the configurable reviewer route`);
|
|
113
|
+
stop();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
run(index + 1);
|
|
117
|
+
})();
|
|
118
|
+
}, TAKEOVER_LATE_RETRY_DELAYS_MS[index]);
|
|
119
|
+
timers.push(timer);
|
|
120
|
+
};
|
|
121
|
+
run(0);
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Install the takeover choreography for an official row that activates later.
|
|
125
|
+
*
|
|
126
|
+
* The plugin manager can re-enable the official row at any time (it writes
|
|
127
|
+
* `disabled: false` into the profile patch layer, which applies after every
|
|
128
|
+
* bundle layer). The `loader/patch-context` listener yields this row's
|
|
129
|
+
* registration **synchronously** before the entry's plugin `apply` runs, so the
|
|
130
|
+
* official row applies cleanly instead of colliding; the reclaim then disables
|
|
131
|
+
* it and takes the registration back.
|
|
132
|
+
*
|
|
133
|
+
* @param ctx - this row's context.
|
|
134
|
+
* @param seam - the Auto-registration seam to yield and reclaim.
|
|
135
|
+
* @returns the guarded reclaim function, so the caller's startup takeover and
|
|
136
|
+
* the listeners share one in-flight guard per entry.
|
|
137
|
+
*/
|
|
138
|
+
function installAutoReviewTakeover(ctx, seam) {
|
|
139
|
+
/** Entries already handled, so a repeated patch-context does not loop. */
|
|
140
|
+
const handled = /* @__PURE__ */ new Set();
|
|
141
|
+
/** Reclaims in flight, shared so two triggers for one entry join one cycle. */
|
|
142
|
+
const reclaiming = /* @__PURE__ */ new Map();
|
|
143
|
+
/** Disable the official row and take the registration back, once per entry. */
|
|
144
|
+
const reclaim = (entry) => {
|
|
145
|
+
const running = reclaiming.get(entry);
|
|
146
|
+
if (running !== void 0) return running;
|
|
147
|
+
const run = reclaimOfficialAutoReviewRow(ctx, entry, seam).finally(() => {
|
|
148
|
+
reclaiming.delete(entry);
|
|
149
|
+
});
|
|
150
|
+
reclaiming.set(entry, run);
|
|
151
|
+
return run;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* The row an event names.
|
|
155
|
+
*
|
|
156
|
+
* The loader hands the emitting entry as the event's `this`, but cordis does
|
|
157
|
+
* not rebind `this` for listeners added through `ctx.on`, so a listener can
|
|
158
|
+
* also receive the payload as its first argument. Both shapes are accepted;
|
|
159
|
+
* the real Host always provides one of them.
|
|
160
|
+
*/
|
|
161
|
+
const entryOfEvent = (self, first) => {
|
|
162
|
+
const candidate = self ?? first;
|
|
163
|
+
return candidate !== null && typeof candidate === "object" && "options" in candidate ? candidate : void 0;
|
|
164
|
+
};
|
|
165
|
+
const listeners = [];
|
|
166
|
+
const listen = (event, listener) => {
|
|
167
|
+
const dispose = ctx.on?.(event, listener);
|
|
168
|
+
if (dispose !== void 0) listeners.push(dispose);
|
|
169
|
+
};
|
|
170
|
+
listen("loader/patch-context", function(first, second) {
|
|
171
|
+
const nextFn = typeof first === "function" ? first : second;
|
|
172
|
+
const entry = entryOfEvent(this, typeof first === "function" ? void 0 : first);
|
|
173
|
+
if (entry === void 0 || typeof nextFn !== "function" || !isOfficialAutoReviewRow(entry) || handled.has(entry)) return nextFn === void 0 ? void 0 : nextFn();
|
|
174
|
+
handled.add(entry);
|
|
175
|
+
seam.yieldRegistration();
|
|
176
|
+
reclaim(entry);
|
|
177
|
+
return nextFn();
|
|
178
|
+
});
|
|
179
|
+
listen("loader/partial-dispose", function(first) {
|
|
180
|
+
const entry = entryOfEvent(this, first);
|
|
181
|
+
if (entry === void 0 || !isOfficialAutoReviewRow(entry)) return;
|
|
182
|
+
handled.delete(entry);
|
|
183
|
+
seam.claim();
|
|
184
|
+
});
|
|
185
|
+
const loader = ctx.get("loader", false);
|
|
186
|
+
if (loader !== void 0 && typeof loader.entries === "function") for (const candidate of loader.entries()) {
|
|
187
|
+
if (!isOfficialAutoReviewRow(candidate)) continue;
|
|
188
|
+
if (alreadyDisabled(candidate)) continue;
|
|
189
|
+
reclaim(candidate);
|
|
190
|
+
}
|
|
191
|
+
const disposable = () => {
|
|
192
|
+
for (const dispose of listeners.splice(0)) dispose();
|
|
193
|
+
};
|
|
194
|
+
const effect = ctx.effect;
|
|
195
|
+
if (effect !== void 0) {
|
|
196
|
+
const registration = effect(function* () {
|
|
197
|
+
yield disposable;
|
|
198
|
+
}, "model-switch: auto-review takeover");
|
|
199
|
+
if (typeof registration === "function") listeners.push(registration);
|
|
200
|
+
}
|
|
201
|
+
return reclaim;
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
3
204
|
//#region lib/types/auto-review.js
|
|
4
205
|
/**
|
|
5
206
|
* Model Switch-owned Auto reviewer row.
|
|
@@ -24,9 +225,14 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
24
225
|
* interference when multiple sessions review tool calls concurrently.
|
|
25
226
|
*
|
|
26
227
|
* The official gate stays an opt-in capability: the row mounts it only when
|
|
27
|
-
* the profile actually started with the experimental-auto-review bundle.
|
|
28
|
-
*
|
|
29
|
-
*
|
|
228
|
+
* the profile actually started with the experimental-auto-review bundle. Two
|
|
229
|
+
* rows cannot share the seam — `permissionPresets.registerAuto` is a single
|
|
230
|
+
* registration and the official probe is unshielded — so this row also
|
|
231
|
+
* **takes the seam over** (see `./auto-review-takeover.ts`): a composed or
|
|
232
|
+
* re-enabled official row is displaced through the released loader seams, the
|
|
233
|
+
* registration is handed over and reclaimed around that displacement, and the
|
|
234
|
+
* gate this row mounts keeps the defensive `permissionPresets` shadow so a
|
|
235
|
+
* degraded preset service can never fail a tool call again.
|
|
30
236
|
*
|
|
31
237
|
* @module dsh-model-switch/auto-review
|
|
32
238
|
*/
|
|
@@ -41,11 +247,18 @@ const name = "model-switch-auto-review";
|
|
|
41
247
|
* every tool call fail with `cannot get property "workingDirectory" without
|
|
42
248
|
* inject`. The gate is now mounted as its own plugin (see {@link autoReviewGate}),
|
|
43
249
|
* so cordis resolves the official dependency list — including future additions —
|
|
44
|
-
* and this row declares only what it reads itself
|
|
250
|
+
* and this row declares only what it reads itself (`loader` drives the
|
|
251
|
+
* takeover).
|
|
45
252
|
*/
|
|
46
|
-
const inject = [
|
|
253
|
+
const inject = [
|
|
254
|
+
"llm",
|
|
255
|
+
"modelSwitch",
|
|
256
|
+
"loader"
|
|
257
|
+
];
|
|
47
258
|
/** Optional bundle whose composition opts a profile into Auto review. */
|
|
48
259
|
const AUTO_REVIEW_BUNDLE = "@deepseek-ai/dsh-experimental-auto-review";
|
|
260
|
+
/** The Auto preset name; `registerAuto` is what publishes it. */
|
|
261
|
+
const AUTO_PRESET = "auto";
|
|
49
262
|
/**
|
|
50
263
|
* Resolve the pinned reviewer route from live settings.
|
|
51
264
|
* @param settings - Model Switch settings carrying the optional reviewer pair.
|
|
@@ -120,17 +333,67 @@ function bindAutoReviewSessionCorrelation(scope, activeSession) {
|
|
|
120
333
|
}, "model-switch: auto-review session correlation");
|
|
121
334
|
}
|
|
122
335
|
/**
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
336
|
+
* Signatures of a broken reviewer seam, never of a real denial.
|
|
337
|
+
*
|
|
338
|
+
* The official gate's `tools/pre-execute` probe resolves its own
|
|
339
|
+
* `sessionProjections` injection before anything can catch it; when that fiber
|
|
340
|
+
* chain breaks it throws `cannot get required service ... in inactive context`
|
|
341
|
+
* (or, on older lines, `cannot get property ... without inject`) straight into
|
|
342
|
+
* the tool call. `TRANSPORT` covers a reviewer that cannot reach its route at
|
|
343
|
+
* all. A denial is a *return* (`{ kind: 'deny' }`), so it never matches.
|
|
128
344
|
*/
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
345
|
+
const REVIEWER_INFRA_FAILURE_SIGNATURES = [
|
|
346
|
+
"inactive context",
|
|
347
|
+
"cannot get required service",
|
|
348
|
+
"cannot get property",
|
|
349
|
+
"without inject",
|
|
350
|
+
"TRANSPORT"
|
|
351
|
+
];
|
|
352
|
+
/**
|
|
353
|
+
* Whether a reviewer-path throw is a broken seam rather than a real decision.
|
|
354
|
+
* @param cause - whatever the waterfall threw.
|
|
355
|
+
* @returns true when the reviewer itself could not run.
|
|
356
|
+
*/
|
|
357
|
+
function isReviewerInfrastructureFailure(cause) {
|
|
358
|
+
const text = cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause);
|
|
359
|
+
return REVIEWER_INFRA_FAILURE_SIGNATURES.some((signature) => text.includes(signature));
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Keep a broken reviewer seam from failing tool calls.
|
|
363
|
+
*
|
|
364
|
+
* Registered on this row's own context through `{ prepend: true }`, so it sits
|
|
365
|
+
* ahead of the official gate's listener and stays live even when the gate could
|
|
366
|
+
* not be mounted (the takeover lost `registerAuto`, or the displaced row has
|
|
367
|
+
* not released it yet). That is the one window the defensive
|
|
368
|
+
* `permissionPresets` shadow cannot cover: the shadow lives in this row's review
|
|
369
|
+
* scope, while the serving gate resolves the real service from the profile.
|
|
370
|
+
*
|
|
371
|
+
* The waterfall only decides whether the body runs; the body dispatches after
|
|
372
|
+
* it returns, so allowing here never masks a tool failure. Reviewer transport
|
|
373
|
+
* and quota errors keep failing closed — only seam breakage is allowed through.
|
|
374
|
+
*
|
|
375
|
+
* @param ctx - this row's context (the listener lives and dies with the plugin).
|
|
376
|
+
* @param options - diagnostics plus the degraded-state predicate.
|
|
377
|
+
*/
|
|
378
|
+
function installToolCallSafetyNet(ctx, options = {}) {
|
|
379
|
+
let reported = false;
|
|
380
|
+
const handler = async (_exec, next) => {
|
|
381
|
+
try {
|
|
382
|
+
return await next();
|
|
383
|
+
} catch (cause) {
|
|
384
|
+
if (!isReviewerInfrastructureFailure(cause)) throw cause;
|
|
385
|
+
if (options.isDegraded !== void 0 && !options.isDegraded()) throw cause;
|
|
386
|
+
if (!reported) {
|
|
387
|
+
reported = true;
|
|
388
|
+
const detail = options.fiberChain === void 0 ? "" : `; fiber chain: ${options.fiberChain()}`;
|
|
389
|
+
ctx.logger.warn(`model-switch: the Auto reviewer path is broken (${String(cause)}); tool calls run unreviewed instead of failing${detail}`);
|
|
390
|
+
}
|
|
391
|
+
return { kind: "allow" };
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
ctx.effect(function* () {
|
|
395
|
+
yield ctx.on("tools/pre-execute", handler, { prepend: true });
|
|
396
|
+
}, "model-switch: reviewer-path safety net");
|
|
134
397
|
}
|
|
135
398
|
/** The preset name the service reports when no configured preset matches. */
|
|
136
399
|
const CUSTOM_PRESET = "custom";
|
|
@@ -144,34 +407,69 @@ function permissionPresetsOf(ctx) {
|
|
|
144
407
|
}
|
|
145
408
|
}
|
|
146
409
|
/**
|
|
410
|
+
* A compact dump of the fiber chain a service resolution walks.
|
|
411
|
+
*
|
|
412
|
+
* `cannot get required service "sessionProjections" in inactive context` is
|
|
413
|
+
* thrown by the first fiber in the caller's chain that declares the service
|
|
414
|
+
* without a resolved snapshot; the chain names that fiber directly.
|
|
415
|
+
* @param ctx - the context whose chain is walked.
|
|
416
|
+
* @returns One `uid/state/name/inject` segment per fiber, outermost last.
|
|
417
|
+
*/
|
|
418
|
+
function fiberChainOf(ctx) {
|
|
419
|
+
const parts = [];
|
|
420
|
+
let node = ctx;
|
|
421
|
+
for (let step = 0; step < 8; step += 1) {
|
|
422
|
+
const fiber = node?.fiber;
|
|
423
|
+
if (fiber === void 0) break;
|
|
424
|
+
const injects = Object.keys(fiber.inject ?? {}).join("|");
|
|
425
|
+
parts.push(`uid=${String(fiber.uid)} state=${String(fiber.state)} name=${String(fiber.name)} inject=[${injects}]`);
|
|
426
|
+
node = fiber.parent;
|
|
427
|
+
}
|
|
428
|
+
return parts.length === 0 ? "no fiber chain" : parts.join(" -> ");
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
147
431
|
* Wrap the permission-preset service so the official gate's per-call probe can
|
|
148
|
-
* never fail a tool call.
|
|
432
|
+
* never fail a tool call, and broker its single Auto registration.
|
|
149
433
|
*
|
|
150
434
|
* DSH's Auto gate starts every `tools/pre-execute` with
|
|
151
435
|
* `permissionPresets.current(session)`, and that method resolves the presets
|
|
152
436
|
* service's own `sessionProjections` injection. When the presets fiber has
|
|
153
437
|
* turned inactive the probe throws `cannot get required service
|
|
154
438
|
* "sessionProjections" in inactive context` outside the gate's reviewer error
|
|
155
|
-
* wrapper, so every tool call dies. This wrapper
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
439
|
+
* wrapper, so every tool call dies. This wrapper resolves the service lazily
|
|
440
|
+
* (so the shadow is in place even while the service is still activating),
|
|
441
|
+
* degrades the probe to `custom` (the gate then skips review and calls
|
|
442
|
+
* `next()`), and reports the lost capability once — with the fiber chain that
|
|
443
|
+
* failed resolution, so the next occurrence names the inactive fiber.
|
|
159
444
|
*
|
|
160
|
-
*
|
|
445
|
+
* `registerAuto` is **brokered**: the gate yields the disposer into its own
|
|
446
|
+
* effect, but the takeover must be able to hand the single registration to a
|
|
447
|
+
* displacing row and take it back without unmounting the gate (the gate's
|
|
448
|
+
* disposal would move live `auto` sessions to `danger-full-access`).
|
|
449
|
+
*
|
|
450
|
+
* @param resolve - resolves the host's permission-preset service per call.
|
|
161
451
|
* @param log - the row logger used for the single degradation report.
|
|
162
|
-
* @
|
|
452
|
+
* @param chain - diagnostic dump of the resolution chain; defaults to empty.
|
|
453
|
+
* @returns A managed preset face.
|
|
163
454
|
*/
|
|
164
|
-
function defensivePermissionPresets(
|
|
455
|
+
function defensivePermissionPresets(resolve, log, chain = () => "") {
|
|
165
456
|
let reported = false;
|
|
457
|
+
let admit;
|
|
458
|
+
let release;
|
|
166
459
|
const degrade = (operation, error) => {
|
|
167
460
|
if (reported) return;
|
|
168
461
|
reported = true;
|
|
169
|
-
log.warn(`model-switch: the permission-preset service refused ${operation} (${String(error)}); the Auto reviewer passes tool calls through instead of blocking them`);
|
|
462
|
+
log.warn(`model-switch: the permission-preset service refused ${operation} (${String(error)}); the Auto reviewer passes tool calls through instead of blocking them [chain: ${chain()}]`);
|
|
463
|
+
};
|
|
464
|
+
const requireService = () => {
|
|
465
|
+
const face = resolve();
|
|
466
|
+
if (face === void 0) throw new Error("permission: the permission-preset service is unavailable");
|
|
467
|
+
return face;
|
|
170
468
|
};
|
|
171
469
|
return {
|
|
172
470
|
current(session) {
|
|
173
471
|
try {
|
|
174
|
-
return
|
|
472
|
+
return requireService().current(session);
|
|
175
473
|
} catch (error) {
|
|
176
474
|
degrade("current()", error);
|
|
177
475
|
return CUSTOM_PRESET;
|
|
@@ -179,22 +477,56 @@ function defensivePermissionPresets(real, log) {
|
|
|
179
477
|
},
|
|
180
478
|
set(session, preset) {
|
|
181
479
|
try {
|
|
182
|
-
return
|
|
480
|
+
return requireService().set(session, preset);
|
|
183
481
|
} catch (error) {
|
|
184
482
|
degrade("set()", error);
|
|
185
483
|
return;
|
|
186
484
|
}
|
|
187
485
|
},
|
|
188
|
-
registerAuto(
|
|
486
|
+
registerAuto(next) {
|
|
487
|
+
admit = next;
|
|
488
|
+
release = requireService().registerAuto(next);
|
|
489
|
+
return () => {
|
|
490
|
+
admit = void 0;
|
|
491
|
+
const dispose = release;
|
|
492
|
+
release = void 0;
|
|
493
|
+
return dispose?.();
|
|
494
|
+
};
|
|
495
|
+
},
|
|
496
|
+
releaseForTakeover() {
|
|
497
|
+
const dispose = release;
|
|
498
|
+
release = void 0;
|
|
499
|
+
dispose?.();
|
|
500
|
+
},
|
|
501
|
+
reclaimRegistration() {
|
|
502
|
+
if (release !== void 0) return true;
|
|
503
|
+
if (admit === void 0) return false;
|
|
189
504
|
try {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return
|
|
505
|
+
release = requireService().registerAuto(admit);
|
|
506
|
+
return true;
|
|
507
|
+
} catch {
|
|
508
|
+
return false;
|
|
194
509
|
}
|
|
510
|
+
},
|
|
511
|
+
get registered() {
|
|
512
|
+
return release !== void 0;
|
|
195
513
|
}
|
|
196
514
|
};
|
|
197
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Build the scoped context the official gate runs under.
|
|
518
|
+
* @param ctx - host context whose modelSwitch settings pin the reviewer route.
|
|
519
|
+
* @param activeSession - optional AsyncLocalStorage correlating each `tools/pre-execute` call to its session.
|
|
520
|
+
* @param presets - optional managed preset face shared with the row's takeover.
|
|
521
|
+
* @returns A child context shadowing the llm service and the permission-preset
|
|
522
|
+
* service (always: a gate that sees the raw service can fail a tool call).
|
|
523
|
+
*/
|
|
524
|
+
function reviewScope(ctx, activeSession = new AsyncLocalStorage(), presets = defensivePermissionPresets(() => permissionPresetsOf(ctx), ctx.logger, () => fiberChainOf(ctx))) {
|
|
525
|
+
return ctx.extend({
|
|
526
|
+
llm: reviewLlm(ctx.llm, () => resolveSessionAutoReviewRoute(ctx, activeSession.getStore())),
|
|
527
|
+
permissionPresets: presets
|
|
528
|
+
});
|
|
529
|
+
}
|
|
198
530
|
/** Structural read of launcher-provided profile facts (the app-boot seam). */
|
|
199
531
|
function autoReviewBundleActive(ctx) {
|
|
200
532
|
let profile;
|
|
@@ -207,6 +539,15 @@ function autoReviewBundleActive(ctx) {
|
|
|
207
539
|
const bundles = profile.startedBundles;
|
|
208
540
|
return Array.isArray(bundles) && bundles.includes(AUTO_REVIEW_BUNDLE);
|
|
209
541
|
}
|
|
542
|
+
/** Structural read of the loader service the takeover drives. */
|
|
543
|
+
function loaderOf(ctx) {
|
|
544
|
+
try {
|
|
545
|
+
const loader = ctx.get("loader", false);
|
|
546
|
+
return loader !== null && typeof loader === "object" ? loader : void 0;
|
|
547
|
+
} catch {
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
}
|
|
210
551
|
/**
|
|
211
552
|
* Wrap the official gate as a plugin that declares its own dependencies.
|
|
212
553
|
*
|
|
@@ -247,11 +588,14 @@ function autoReviewGate(gate, hooks = {}) {
|
|
|
247
588
|
*/
|
|
248
589
|
let lastGateFiber;
|
|
249
590
|
/**
|
|
250
|
-
* Mount the official gate under the reviewer-route scope.
|
|
591
|
+
* Mount the official gate under the reviewer-route scope and own the seam.
|
|
251
592
|
*
|
|
252
593
|
* A profile that never enabled the optional bundle keeps no Auto preset and no
|
|
253
594
|
* reviewer; a deployment missing the official package reports the lost
|
|
254
|
-
* capability explicitly instead of guessing another path.
|
|
595
|
+
* capability explicitly instead of guessing another path. A composed (or
|
|
596
|
+
* plugin-manager re-enabled) official row is displaced first — see
|
|
597
|
+
* `./auto-review-takeover.ts` for the choreography and why the yield is
|
|
598
|
+
* synchronous.
|
|
255
599
|
*/
|
|
256
600
|
async function apply(ctx) {
|
|
257
601
|
if (!autoReviewBundleActive(ctx)) {
|
|
@@ -266,42 +610,109 @@ async function apply(ctx) {
|
|
|
266
610
|
ctx.logger.error(cause);
|
|
267
611
|
return;
|
|
268
612
|
}
|
|
269
|
-
const
|
|
270
|
-
lastGateFiber = void 0;
|
|
271
|
-
if (previous !== void 0) try {
|
|
272
|
-
await previous.dispose();
|
|
273
|
-
} catch (cause) {
|
|
274
|
-
ctx.logger.info("model-switch: the previous auto-review gate had already unloaded; continuing with the new one");
|
|
275
|
-
ctx.logger.info(String(cause));
|
|
276
|
-
}
|
|
613
|
+
const presets = defensivePermissionPresets(() => permissionPresetsOf(ctx), ctx.logger, () => fiberChainOf(ctx));
|
|
277
614
|
const activeSession = new AsyncLocalStorage();
|
|
278
|
-
const scope = reviewScope(ctx, activeSession);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
615
|
+
const scope = reviewScope(ctx, activeSession, presets);
|
|
616
|
+
installToolCallSafetyNet(ctx, {
|
|
617
|
+
fiberChain: () => fiberChainOf(ctx),
|
|
618
|
+
isDegraded: () => !presets.registered
|
|
619
|
+
});
|
|
620
|
+
let gateReady = false;
|
|
621
|
+
let mountInFlight;
|
|
622
|
+
/** Sessions currently on the Auto preset (best-effort). */
|
|
623
|
+
const rememberAuto = () => {
|
|
624
|
+
const remembered = [];
|
|
625
|
+
try {
|
|
626
|
+
const sessions = ctx.get("sessions", false);
|
|
627
|
+
for (const session of sessions?.list?.() ?? []) if (presets.current(session) === AUTO_PRESET) remembered.push(session);
|
|
628
|
+
} catch {}
|
|
629
|
+
return remembered;
|
|
630
|
+
};
|
|
631
|
+
const restoreAuto = (remembered) => {
|
|
632
|
+
for (const session of remembered) try {
|
|
633
|
+
presets.set(session, AUTO_PRESET);
|
|
634
|
+
} catch {}
|
|
635
|
+
};
|
|
636
|
+
/**
|
|
637
|
+
* Run one displacement step without losing the sessions' Auto selection.
|
|
638
|
+
*
|
|
639
|
+
* Disposing a gate moves live `auto` sessions to `danger-full-access` by
|
|
640
|
+
* design (the official teardown runs on both the displaced row and this
|
|
641
|
+
* row's own re-mounts); the remembered selection is restored afterwards.
|
|
642
|
+
*/
|
|
643
|
+
const preserveAuto = async (action) => {
|
|
644
|
+
const remembered = rememberAuto();
|
|
645
|
+
await action();
|
|
646
|
+
restoreAuto(remembered);
|
|
647
|
+
};
|
|
648
|
+
/** Mount this row's shielded gate exactly once; failures dispose and report. */
|
|
649
|
+
const mountGate = () => {
|
|
650
|
+
mountInFlight ??= (async () => {
|
|
651
|
+
let outcome = "failed";
|
|
652
|
+
await preserveAuto(async () => {
|
|
653
|
+
const previous = lastGateFiber;
|
|
654
|
+
lastGateFiber = void 0;
|
|
655
|
+
gateReady = false;
|
|
656
|
+
if (previous !== void 0) try {
|
|
657
|
+
await previous.dispose();
|
|
658
|
+
} catch (cause) {
|
|
659
|
+
ctx.logger.info("model-switch: the previous auto-review gate had already unloaded; continuing with the new one");
|
|
660
|
+
ctx.logger.info(String(cause));
|
|
661
|
+
}
|
|
662
|
+
let failure;
|
|
663
|
+
let applied = false;
|
|
664
|
+
const fiber = scope.plugin(autoReviewGate(official, {
|
|
665
|
+
onApplied: () => {
|
|
666
|
+
applied = true;
|
|
667
|
+
bindAutoReviewSessionCorrelation(scope, activeSession);
|
|
668
|
+
},
|
|
669
|
+
onFailure: (error) => {
|
|
670
|
+
failure = error;
|
|
671
|
+
}
|
|
672
|
+
}));
|
|
673
|
+
try {
|
|
674
|
+
await fiber;
|
|
675
|
+
} catch (cause) {
|
|
676
|
+
failure ??= cause;
|
|
677
|
+
}
|
|
678
|
+
if (failure !== void 0) {
|
|
679
|
+
try {
|
|
680
|
+
await fiber.dispose();
|
|
681
|
+
} catch {}
|
|
682
|
+
ctx.logger.warn(`model-switch: the Auto reviewer gate did not mount (${String(failure)}); the displaced registration may still be releasing`);
|
|
683
|
+
outcome = "failed";
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
if (!applied) {
|
|
687
|
+
ctx.logger.info("model-switch: the official auto-review gate is waiting for its host services; the reviewer route override is not bound yet");
|
|
688
|
+
outcome = "waiting";
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
lastGateFiber = fiber;
|
|
692
|
+
gateReady = true;
|
|
693
|
+
outcome = "applied";
|
|
694
|
+
});
|
|
695
|
+
return outcome;
|
|
696
|
+
})().finally(() => {
|
|
697
|
+
mountInFlight = void 0;
|
|
698
|
+
});
|
|
699
|
+
return mountInFlight;
|
|
700
|
+
};
|
|
701
|
+
/** Make this row's Auto registration live; true once it is held. */
|
|
702
|
+
const claim = async () => {
|
|
703
|
+
if (presets.registered) return true;
|
|
704
|
+
if (gateReady) return presets.reclaimRegistration();
|
|
705
|
+
return await mountGate() !== "failed";
|
|
706
|
+
};
|
|
707
|
+
const reclaim = installAutoReviewTakeover(ctx, {
|
|
708
|
+
yieldRegistration: () => presets.releaseForTakeover(),
|
|
709
|
+
claim,
|
|
710
|
+
preserveAuto
|
|
711
|
+
});
|
|
712
|
+
const entry = officialAutoReviewEntry(loaderOf(ctx));
|
|
713
|
+
if (entry !== void 0 && entry.disabled !== true) await reclaim(entry);
|
|
714
|
+
else await claim();
|
|
715
|
+
if (!presets.registered) ctx.logger.error("model-switch: the Auto registration is not held by this row, so Auto review runs without the configurable reviewer route; the reviewer path is shielded instead of failing tool calls. Restart DSH to converge the reviewer row — a hot install cannot always displace the official row in time");
|
|
305
716
|
}
|
|
306
717
|
//#endregion
|
|
307
|
-
export { apply, autoReviewGate, autoReviewRouteOf, bindAutoReviewSessionCorrelation, defensivePermissionPresets, inject, name, resolveSessionAutoReviewRoute, reviewLlm, reviewScope };
|
|
718
|
+
export { apply, autoReviewGate, autoReviewRouteOf, bindAutoReviewSessionCorrelation, defensivePermissionPresets, inject, installToolCallSafetyNet, isReviewerInfrastructureFailure, name, resolveSessionAutoReviewRoute, reviewLlm, reviewScope };
|
package/lib/client.js
CHANGED
|
@@ -1800,6 +1800,10 @@ window.__ModuleLoader__.load({
|
|
|
1800
1800
|
className: ModelSwitchSettings_module_css_default.hint,
|
|
1801
1801
|
children: props.t("loading")
|
|
1802
1802
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1803
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1804
|
+
className: ModelSwitchSettings_module_css_default.hint,
|
|
1805
|
+
children: props.t("autoReviewTakeoverNote")
|
|
1806
|
+
}),
|
|
1803
1807
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1804
1808
|
className: ModelSwitchSettings_module_css_default.hint,
|
|
1805
1809
|
children: props.t("autoReviewRouteHelp")
|
|
@@ -2283,8 +2287,9 @@ window.__ModuleLoader__.load({
|
|
|
2283
2287
|
catalogFailed: "无法加载模型目录。",
|
|
2284
2288
|
autoReview: "自动审查模型",
|
|
2285
2289
|
autoReviewFollow: "跟随会话模型",
|
|
2286
|
-
autoReviewHelp: "为官方 auto-review(Auto 权限预设的逐调用审查)指定审查模型;需启用官方 auto-review
|
|
2290
|
+
autoReviewHelp: "为官方 auto-review(Auto 权限预设的逐调用审查)指定审查模型;需启用官方 auto-review 能力包。关闭时审查跟随当前会话模型。本插件启用时,官方「自动审查」组件由本插件接管。",
|
|
2287
2291
|
autoReviewRouteHelp: "审查请求固定使用这里的 Provider/模型,与会话模型无关;保存后对下一次审查立即生效。",
|
|
2292
|
+
autoReviewTakeoverNote: "官方「自动审查」组件已由本插件接管(提供可配置审查路由);在插件管理里把它重新打开会被再次关闭,并在日志里留下一条记录。",
|
|
2288
2293
|
compaction: "压缩模型",
|
|
2289
2294
|
compactionFollow: "跟随会话模型",
|
|
2290
2295
|
compactionHelp: "为官方压缩(/compact 与官方自动压缩)指定摘要模型;关闭(两个字段都留空)时使用官方默认,即当前会话的目标模型。",
|
|
@@ -2341,8 +2346,9 @@ window.__ModuleLoader__.load({
|
|
|
2341
2346
|
catalogFailed: "Could not load the model catalog.",
|
|
2342
2347
|
autoReview: "Auto review model",
|
|
2343
2348
|
autoReviewFollow: "Follows the session model",
|
|
2344
|
-
autoReviewHelp: "Pin the reviewer model for the official auto-review gate (per-call review under the Auto permission preset). Requires the official auto-review bundle. When off, reviews follow the session model.",
|
|
2349
|
+
autoReviewHelp: "Pin the reviewer model for the official auto-review gate (per-call review under the Auto permission preset). Requires the official auto-review bundle. When off, reviews follow the session model. With this plugin enabled the official auto-review component is taken over.",
|
|
2345
2350
|
autoReviewRouteHelp: "Review requests use exactly this provider/model regardless of the session model; saved changes apply to the very next review.",
|
|
2351
|
+
autoReviewTakeoverNote: "The official auto-review component is taken over by this plugin (configurable reviewer route); toggling it back on in the plugin manager is re-disabled, with one log line.",
|
|
2346
2352
|
compaction: "Compaction model",
|
|
2347
2353
|
compactionFollow: "Follows the conversation model",
|
|
2348
2354
|
compactionHelp: "Pick the summarization model for official compaction (/compact and official automatic compaction). When off (both fields empty), the official default applies: the conversation target model.",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model Switch's takeover of the official Auto-review row.
|
|
3
|
+
*
|
|
4
|
+
* `permissionPresets.registerAuto` is a single registration, and the official
|
|
5
|
+
* gate's `tools/pre-execute` probe is unshielded — it resolves the presets
|
|
6
|
+
* service's `sessionProjections` injection on every tool call and dies with
|
|
7
|
+
* `cannot get required service "sessionProjections" in inactive context` when
|
|
8
|
+
* that chain breaks. Two live rows are therefore not survivable: the row that
|
|
9
|
+
* loses `registerAuto` fails mid-effect (its probe listener half-applied, every
|
|
10
|
+
* later tool call failing on the raw probe error), and the row that wins runs
|
|
11
|
+
* unguarded. The plugin-manager toggle makes this reachable at runtime by
|
|
12
|
+
* writing `disabled: false` for the official row into the profile patch layer.
|
|
13
|
+
*
|
|
14
|
+
* This module keeps exactly one row on the seam by taking the official row out
|
|
15
|
+
* of the composition and letting this plugin's row claim the registration —
|
|
16
|
+
* the same released-seam choreography the session-title takeover uses:
|
|
17
|
+
* `loader/patch-context`, `Entry.update`, `loader/partial-dispose`.
|
|
18
|
+
*
|
|
19
|
+
* The yield is **synchronous** on purpose: `loader/patch-context` is a
|
|
20
|
+
* synchronous waterfall whose `next()` returns straight into the entry's
|
|
21
|
+
* `registry.plugin(...)` mount, so the official `registerAuto` runs in the same
|
|
22
|
+
* turn. An awaited yield would still collide; a synchronous release hands the
|
|
23
|
+
* registration over cleanly (the official row then applies in full and is
|
|
24
|
+
* disposed cleanly, never half-applied).
|
|
25
|
+
*
|
|
26
|
+
* @module dsh-model-switch/auto-review-takeover
|
|
27
|
+
*/
|
|
28
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
29
|
+
import type { Entry, Loader } from '@deepseek-ai/cordis-plugin-loader';
|
|
30
|
+
/** The loader row id the official Auto-review bundle declares. */
|
|
31
|
+
export declare const OFFICIAL_AUTO_REVIEW_ROW = "auto-review";
|
|
32
|
+
/**
|
|
33
|
+
* The official package identity — the only handle on the row this plugin
|
|
34
|
+
* trusts. Matching by id alone could reach a group or another plugin's row, and
|
|
35
|
+
* a matched row is disabled (its fiber disposed).
|
|
36
|
+
*/
|
|
37
|
+
export declare const OFFICIAL_AUTO_REVIEW_PACKAGE = "@deepseek-ai/dsh-experimental-auto-review";
|
|
38
|
+
/** Bounded takeover attempts before the seam is reported lost. */
|
|
39
|
+
export declare const TAKEOVER_MAX_ATTEMPTS = 60;
|
|
40
|
+
/** Delay between takeover attempts; the first attempt is immediate. */
|
|
41
|
+
export declare const TAKEOVER_RETRY_DELAY_MS = 50;
|
|
42
|
+
/**
|
|
43
|
+
* Delays for the late re-claims after the attempt loop gave up (ms).
|
|
44
|
+
*
|
|
45
|
+
* `Entry.update({ disabled: true })` starts the displaced fiber's disposal but
|
|
46
|
+
* the loader does not await it, and a disposal that tears down a mounted gate,
|
|
47
|
+
* restores permission presets and unwinds fibers can take longer than the
|
|
48
|
+
* attempt loop's budget. These re-claims keep trying for a few more seconds so a
|
|
49
|
+
* hot-swapped install converges instead of needing a restart.
|
|
50
|
+
*/
|
|
51
|
+
export declare const TAKEOVER_LATE_RETRY_DELAYS_MS: readonly [500, 1000, 1500];
|
|
52
|
+
/** The seam this row's gate owns; the takeover drives it around the official row. */
|
|
53
|
+
export interface AutoReviewSeam {
|
|
54
|
+
/**
|
|
55
|
+
* Synchronously free the Auto registration so a displacing row can register
|
|
56
|
+
* cleanly. Keeping the admit callback is the seam's business; claiming puts
|
|
57
|
+
* the registration back.
|
|
58
|
+
*/
|
|
59
|
+
yieldRegistration(): void;
|
|
60
|
+
/** Make this row's Auto registration live; true once it is held. */
|
|
61
|
+
claim(): Promise<boolean>;
|
|
62
|
+
/** Run one takeover step without losing the sessions' Auto selection. */
|
|
63
|
+
preserveAuto(action: () => Promise<void>): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
/** Whether one loader entry is the official Auto-review row (strict package identity). */
|
|
66
|
+
export declare function isOfficialAutoReviewRow(entry: Entry | undefined): boolean;
|
|
67
|
+
/** The first composed official Auto-review row, if the profile declares one. */
|
|
68
|
+
export declare function officialAutoReviewEntry(loader: Loader | undefined): Entry | undefined;
|
|
69
|
+
/**
|
|
70
|
+
* Displace the official Auto-review row and take the registration back.
|
|
71
|
+
*
|
|
72
|
+
* The official row is disabled through the public `Entry.update` (which starts
|
|
73
|
+
* its fiber's disposal — the loader does not await it, so the claim retries
|
|
74
|
+
* until the displaced registration actually released). The Auto selection of
|
|
75
|
+
* live sessions is preserved around the displacement: the official gate's
|
|
76
|
+
* disposal moves `auto` sessions to `danger-full-access` by design, and this
|
|
77
|
+
* row's registration puts them back.
|
|
78
|
+
*
|
|
79
|
+
* @param ctx - this row's context, used only for logging.
|
|
80
|
+
* @param entry - the official row to displace.
|
|
81
|
+
* @param seam - the Auto-registration seam to yield and reclaim.
|
|
82
|
+
* @returns a promise settling once the takeover attempt loop finished.
|
|
83
|
+
*/
|
|
84
|
+
export declare function reclaimOfficialAutoReviewRow(ctx: Context, entry: Entry, seam: AutoReviewSeam): Promise<void>;
|
|
85
|
+
/**
|
|
86
|
+
* Install the takeover choreography for an official row that activates later.
|
|
87
|
+
*
|
|
88
|
+
* The plugin manager can re-enable the official row at any time (it writes
|
|
89
|
+
* `disabled: false` into the profile patch layer, which applies after every
|
|
90
|
+
* bundle layer). The `loader/patch-context` listener yields this row's
|
|
91
|
+
* registration **synchronously** before the entry's plugin `apply` runs, so the
|
|
92
|
+
* official row applies cleanly instead of colliding; the reclaim then disables
|
|
93
|
+
* it and takes the registration back.
|
|
94
|
+
*
|
|
95
|
+
* @param ctx - this row's context.
|
|
96
|
+
* @param seam - the Auto-registration seam to yield and reclaim.
|
|
97
|
+
* @returns the guarded reclaim function, so the caller's startup takeover and
|
|
98
|
+
* the listeners share one in-flight guard per entry.
|
|
99
|
+
*/
|
|
100
|
+
export declare function installAutoReviewTakeover(ctx: Context, seam: AutoReviewSeam): (entry: Entry) => Promise<void>;
|
|
@@ -21,9 +21,14 @@
|
|
|
21
21
|
* interference when multiple sessions review tool calls concurrently.
|
|
22
22
|
*
|
|
23
23
|
* The official gate stays an opt-in capability: the row mounts it only when
|
|
24
|
-
* the profile actually started with the experimental-auto-review bundle.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* the profile actually started with the experimental-auto-review bundle. Two
|
|
25
|
+
* rows cannot share the seam — `permissionPresets.registerAuto` is a single
|
|
26
|
+
* registration and the official probe is unshielded — so this row also
|
|
27
|
+
* **takes the seam over** (see `./auto-review-takeover.ts`): a composed or
|
|
28
|
+
* re-enabled official row is displaced through the released loader seams, the
|
|
29
|
+
* registration is handed over and reclaimed around that displacement, and the
|
|
30
|
+
* gate this row mounts keeps the defensive `permissionPresets` shadow so a
|
|
31
|
+
* degraded preset service can never fail a tool call again.
|
|
27
32
|
*
|
|
28
33
|
* @module dsh-model-switch/auto-review
|
|
29
34
|
*/
|
|
@@ -42,7 +47,8 @@ export declare const name = "model-switch-auto-review";
|
|
|
42
47
|
* every tool call fail with `cannot get property "workingDirectory" without
|
|
43
48
|
* inject`. The gate is now mounted as its own plugin (see {@link autoReviewGate}),
|
|
44
49
|
* so cordis resolves the official dependency list — including future additions —
|
|
45
|
-
* and this row declares only what it reads itself
|
|
50
|
+
* and this row declares only what it reads itself (`loader` drives the
|
|
51
|
+
* takeover).
|
|
46
52
|
*/
|
|
47
53
|
export declare const inject: string[];
|
|
48
54
|
/** Pinned reviewer route; both identifiers are non-empty. */
|
|
@@ -97,40 +103,95 @@ export declare function resolveSessionAutoReviewRoute(ctx: Context, session?: Se
|
|
|
97
103
|
*/
|
|
98
104
|
export declare function bindAutoReviewSessionCorrelation(scope: Context, activeSession: AsyncLocalStorage<Session | undefined>): void;
|
|
99
105
|
/**
|
|
100
|
-
*
|
|
101
|
-
* @param
|
|
102
|
-
* @
|
|
103
|
-
* @returns A child context shadowing the llm service and, when the permission
|
|
104
|
-
* preset service is reachable, that service too.
|
|
106
|
+
* Whether a reviewer-path throw is a broken seam rather than a real decision.
|
|
107
|
+
* @param cause - whatever the waterfall threw.
|
|
108
|
+
* @returns true when the reviewer itself could not run.
|
|
105
109
|
*/
|
|
106
|
-
export declare function
|
|
110
|
+
export declare function isReviewerInfrastructureFailure(cause: unknown): boolean;
|
|
111
|
+
/**
|
|
112
|
+
* Keep a broken reviewer seam from failing tool calls.
|
|
113
|
+
*
|
|
114
|
+
* Registered on this row's own context through `{ prepend: true }`, so it sits
|
|
115
|
+
* ahead of the official gate's listener and stays live even when the gate could
|
|
116
|
+
* not be mounted (the takeover lost `registerAuto`, or the displaced row has
|
|
117
|
+
* not released it yet). That is the one window the defensive
|
|
118
|
+
* `permissionPresets` shadow cannot cover: the shadow lives in this row's review
|
|
119
|
+
* scope, while the serving gate resolves the real service from the profile.
|
|
120
|
+
*
|
|
121
|
+
* The waterfall only decides whether the body runs; the body dispatches after
|
|
122
|
+
* it returns, so allowing here never masks a tool failure. Reviewer transport
|
|
123
|
+
* and quota errors keep failing closed — only seam breakage is allowed through.
|
|
124
|
+
*
|
|
125
|
+
* @param ctx - this row's context (the listener lives and dies with the plugin).
|
|
126
|
+
* @param options - diagnostics plus the degraded-state predicate.
|
|
127
|
+
*/
|
|
128
|
+
export declare function installToolCallSafetyNet(ctx: Context, options?: {
|
|
129
|
+
/** Diagnostic fiber dump appended to the first report. */
|
|
130
|
+
fiberChain?: (() => string) | undefined;
|
|
131
|
+
/**
|
|
132
|
+
* Whether this row currently knows the reviewer route is not held.
|
|
133
|
+
*
|
|
134
|
+
* The net only fails open in that state: another plugin's broken listener
|
|
135
|
+
* must keep failing closed, and a held route already has the defensive
|
|
136
|
+
* shadow in front of it. Defaults to always degraded (direct callers).
|
|
137
|
+
*/
|
|
138
|
+
isDegraded?: (() => boolean) | undefined;
|
|
139
|
+
}): void;
|
|
107
140
|
/** The permission-preset face the official gate reads; structural so a missing package never breaks a build. */
|
|
108
141
|
export interface PermissionPresetsFace {
|
|
109
142
|
current(session: Session): string;
|
|
110
143
|
set(session: Session, preset: string): unknown;
|
|
111
144
|
registerAuto(admit: () => void): unknown;
|
|
112
145
|
}
|
|
146
|
+
/**
|
|
147
|
+
* The managed face: what the gate sees, plus the registration handle the
|
|
148
|
+
* takeover drives.
|
|
149
|
+
*/
|
|
150
|
+
export interface ManagedPermissionPresetsFace extends PermissionPresetsFace {
|
|
151
|
+
/** Synchronously release the live Auto registration, keeping the admit callback. */
|
|
152
|
+
releaseForTakeover(): void;
|
|
153
|
+
/** Take the Auto registration back; true once it is live. */
|
|
154
|
+
reclaimRegistration(): boolean;
|
|
155
|
+
/** Whether this face currently holds the Auto registration. */
|
|
156
|
+
readonly registered: boolean;
|
|
157
|
+
}
|
|
113
158
|
/**
|
|
114
159
|
* Wrap the permission-preset service so the official gate's per-call probe can
|
|
115
|
-
* never fail a tool call.
|
|
160
|
+
* never fail a tool call, and broker its single Auto registration.
|
|
116
161
|
*
|
|
117
162
|
* DSH's Auto gate starts every `tools/pre-execute` with
|
|
118
163
|
* `permissionPresets.current(session)`, and that method resolves the presets
|
|
119
164
|
* service's own `sessionProjections` injection. When the presets fiber has
|
|
120
165
|
* turned inactive the probe throws `cannot get required service
|
|
121
166
|
* "sessionProjections" in inactive context` outside the gate's reviewer error
|
|
122
|
-
* wrapper, so every tool call dies. This wrapper
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
167
|
+
* wrapper, so every tool call dies. This wrapper resolves the service lazily
|
|
168
|
+
* (so the shadow is in place even while the service is still activating),
|
|
169
|
+
* degrades the probe to `custom` (the gate then skips review and calls
|
|
170
|
+
* `next()`), and reports the lost capability once — with the fiber chain that
|
|
171
|
+
* failed resolution, so the next occurrence names the inactive fiber.
|
|
172
|
+
*
|
|
173
|
+
* `registerAuto` is **brokered**: the gate yields the disposer into its own
|
|
174
|
+
* effect, but the takeover must be able to hand the single registration to a
|
|
175
|
+
* displacing row and take it back without unmounting the gate (the gate's
|
|
176
|
+
* disposal would move live `auto` sessions to `danger-full-access`).
|
|
177
|
+
*
|
|
178
|
+
* @param resolve - resolves the host's permission-preset service per call.
|
|
128
179
|
* @param log - the row logger used for the single degradation report.
|
|
129
|
-
* @
|
|
180
|
+
* @param chain - diagnostic dump of the resolution chain; defaults to empty.
|
|
181
|
+
* @returns A managed preset face.
|
|
130
182
|
*/
|
|
131
|
-
export declare function defensivePermissionPresets(
|
|
183
|
+
export declare function defensivePermissionPresets(resolve: () => PermissionPresetsFace | undefined, log: {
|
|
132
184
|
warn(line: string): void;
|
|
133
|
-
}):
|
|
185
|
+
}, chain?: () => string): ManagedPermissionPresetsFace;
|
|
186
|
+
/**
|
|
187
|
+
* Build the scoped context the official gate runs under.
|
|
188
|
+
* @param ctx - host context whose modelSwitch settings pin the reviewer route.
|
|
189
|
+
* @param activeSession - optional AsyncLocalStorage correlating each `tools/pre-execute` call to its session.
|
|
190
|
+
* @param presets - optional managed preset face shared with the row's takeover.
|
|
191
|
+
* @returns A child context shadowing the llm service and the permission-preset
|
|
192
|
+
* service (always: a gate that sees the raw service can fail a tool call).
|
|
193
|
+
*/
|
|
194
|
+
export declare function reviewScope(ctx: Context, activeSession?: AsyncLocalStorage<Session | undefined>, presets?: ManagedPermissionPresetsFace): Context;
|
|
134
195
|
/**
|
|
135
196
|
* Structural view of the official gate's plugin face.
|
|
136
197
|
*
|
|
@@ -168,10 +229,13 @@ export declare function autoReviewGate(gate: OfficialAutoReviewGate, hooks?: {
|
|
|
168
229
|
readonly onFailure?: (error: unknown) => void;
|
|
169
230
|
}): MountedAutoReviewGate;
|
|
170
231
|
/**
|
|
171
|
-
* Mount the official gate under the reviewer-route scope.
|
|
232
|
+
* Mount the official gate under the reviewer-route scope and own the seam.
|
|
172
233
|
*
|
|
173
234
|
* A profile that never enabled the optional bundle keeps no Auto preset and no
|
|
174
235
|
* reviewer; a deployment missing the official package reports the lost
|
|
175
|
-
* capability explicitly instead of guessing another path.
|
|
236
|
+
* capability explicitly instead of guessing another path. A composed (or
|
|
237
|
+
* plugin-manager re-enabled) official row is displaced first — see
|
|
238
|
+
* `./auto-review-takeover.ts` for the choreography and why the yield is
|
|
239
|
+
* synchronous.
|
|
176
240
|
*/
|
|
177
241
|
export declare function apply(ctx: Context): Promise<void>;
|
|
@@ -30,8 +30,9 @@ export declare const zh: {
|
|
|
30
30
|
readonly catalogFailed: "无法加载模型目录。";
|
|
31
31
|
readonly autoReview: "自动审查模型";
|
|
32
32
|
readonly autoReviewFollow: "跟随会话模型";
|
|
33
|
-
readonly autoReviewHelp: "为官方 auto-review(Auto 权限预设的逐调用审查)指定审查模型;需启用官方 auto-review
|
|
33
|
+
readonly autoReviewHelp: "为官方 auto-review(Auto 权限预设的逐调用审查)指定审查模型;需启用官方 auto-review 能力包。关闭时审查跟随当前会话模型。本插件启用时,官方「自动审查」组件由本插件接管。";
|
|
34
34
|
readonly autoReviewRouteHelp: "审查请求固定使用这里的 Provider/模型,与会话模型无关;保存后对下一次审查立即生效。";
|
|
35
|
+
readonly autoReviewTakeoverNote: "官方「自动审查」组件已由本插件接管(提供可配置审查路由);在插件管理里把它重新打开会被再次关闭,并在日志里留下一条记录。";
|
|
35
36
|
readonly compaction: "压缩模型";
|
|
36
37
|
readonly compactionFollow: "跟随会话模型";
|
|
37
38
|
readonly compactionHelp: "为官方压缩(/compact 与官方自动压缩)指定摘要模型;关闭(两个字段都留空)时使用官方默认,即当前会话的目标模型。";
|