dsh-model-switch 0.4.30 → 0.4.31
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 +13 -0
- package/README.md +4 -2
- package/README.zh.md +4 -2
- package/lib/auto-review.js +355 -71
- package/lib/client.js +8 -2
- package/lib/types/auto-review-takeover.d.ts +90 -0
- package/lib/types/auto-review.d.ts +53 -24
- package/lib/types/client/locales.d.ts +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
## v0.4.31
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
- **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.
|
|
6
|
+
- **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.
|
|
7
|
+
- **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`.
|
|
8
|
+
- **`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.
|
|
9
|
+
- **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.
|
|
10
|
+
- **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.
|
|
11
|
+
- **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.
|
|
12
|
+
- **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.
|
|
13
|
+
|
|
1
14
|
## v0.4.30
|
|
2
15
|
|
|
3
16
|
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
|
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)
|
package/lib/auto-review.js
CHANGED
|
@@ -1,5 +1,146 @@
|
|
|
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
|
+
/** Whether one loader entry is the official Auto-review row (strict package identity). */
|
|
10
|
+
function isOfficialAutoReviewRow(entry) {
|
|
11
|
+
if (entry === void 0) return false;
|
|
12
|
+
const pkg = entry.options?.name;
|
|
13
|
+
return typeof pkg === "string" && pkg === "@deepseek-ai/dsh-experimental-auto-review";
|
|
14
|
+
}
|
|
15
|
+
/** The first composed official Auto-review row, if the profile declares one. */
|
|
16
|
+
function officialAutoReviewEntry(loader) {
|
|
17
|
+
if (loader === void 0 || typeof loader.entries !== "function") return void 0;
|
|
18
|
+
for (const entry of loader.entries()) if (isOfficialAutoReviewRow(entry)) return entry;
|
|
19
|
+
}
|
|
20
|
+
/** The id one entry is known by (the loader's leaf id, not a nested path). */
|
|
21
|
+
function leafId(entry) {
|
|
22
|
+
const raw = entry.options?.id;
|
|
23
|
+
return typeof raw === "string" ? raw : void 0;
|
|
24
|
+
}
|
|
25
|
+
/** A loader entry the loader has not created a fiber for yet. */
|
|
26
|
+
function alreadyDisabled(entry) {
|
|
27
|
+
return entry.disabled === true;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Displace the official Auto-review row and take the registration back.
|
|
31
|
+
*
|
|
32
|
+
* The official row is disabled through the public `Entry.update` (which starts
|
|
33
|
+
* its fiber's disposal — the loader does not await it, so the claim retries
|
|
34
|
+
* until the displaced registration actually released). The Auto selection of
|
|
35
|
+
* live sessions is preserved around the displacement: the official gate's
|
|
36
|
+
* disposal moves `auto` sessions to `danger-full-access` by design, and this
|
|
37
|
+
* row's registration puts them back.
|
|
38
|
+
*
|
|
39
|
+
* @param ctx - this row's context, used only for logging.
|
|
40
|
+
* @param entry - the official row to displace.
|
|
41
|
+
* @param seam - the Auto-registration seam to yield and reclaim.
|
|
42
|
+
* @returns a promise settling once the takeover attempt loop finished.
|
|
43
|
+
*/
|
|
44
|
+
async function reclaimOfficialAutoReviewRow(ctx, entry, seam) {
|
|
45
|
+
await seam.preserveAuto(async () => {
|
|
46
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
47
|
+
await new Promise((resolve) => setTimeout(resolve, attempt === 0 ? 0 : 10));
|
|
48
|
+
try {
|
|
49
|
+
await entry.update({ disabled: true });
|
|
50
|
+
ctx.logger.info(`model-switch: disabled the official auto-review row ${String(leafId(entry) ?? "auto-review")} (${OFFICIAL_AUTO_REVIEW_PACKAGE}); Model Switch's auto-review row owns the reviewer seam (configurable reviewer route)`);
|
|
51
|
+
} catch (cause) {
|
|
52
|
+
ctx.logger.info("model-switch: the displaced auto-review row could not be persisted as disabled; the takeover repeats next startup");
|
|
53
|
+
ctx.logger.info(String(cause));
|
|
54
|
+
}
|
|
55
|
+
if (await seam.claim()) {
|
|
56
|
+
ctx.logger.info(`model-switch: displaced the official auto-review row ${String(leafId(entry) ?? "auto-review")}; Auto review runs under the configurable reviewer route`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
ctx.logger.error(`model-switch: the official auto-review row ${String(leafId(entry) ?? "auto-review")} kept the Auto registration; the reviewer route override is unavailable this session`);
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Install the takeover choreography for an official row that activates later.
|
|
65
|
+
*
|
|
66
|
+
* The plugin manager can re-enable the official row at any time (it writes
|
|
67
|
+
* `disabled: false` into the profile patch layer, which applies after every
|
|
68
|
+
* bundle layer). The `loader/patch-context` listener yields this row's
|
|
69
|
+
* registration **synchronously** before the entry's plugin `apply` runs, so the
|
|
70
|
+
* official row applies cleanly instead of colliding; the reclaim then disables
|
|
71
|
+
* it and takes the registration back.
|
|
72
|
+
*
|
|
73
|
+
* @param ctx - this row's context.
|
|
74
|
+
* @param seam - the Auto-registration seam to yield and reclaim.
|
|
75
|
+
* @returns the guarded reclaim function, so the caller's startup takeover and
|
|
76
|
+
* the listeners share one in-flight guard per entry.
|
|
77
|
+
*/
|
|
78
|
+
function installAutoReviewTakeover(ctx, seam) {
|
|
79
|
+
/** Entries already handled, so a repeated patch-context does not loop. */
|
|
80
|
+
const handled = /* @__PURE__ */ new Set();
|
|
81
|
+
/** Reclaims in flight, shared so two triggers for one entry join one cycle. */
|
|
82
|
+
const reclaiming = /* @__PURE__ */ new Map();
|
|
83
|
+
/** Disable the official row and take the registration back, once per entry. */
|
|
84
|
+
const reclaim = (entry) => {
|
|
85
|
+
const running = reclaiming.get(entry);
|
|
86
|
+
if (running !== void 0) return running;
|
|
87
|
+
const run = reclaimOfficialAutoReviewRow(ctx, entry, seam).finally(() => {
|
|
88
|
+
reclaiming.delete(entry);
|
|
89
|
+
});
|
|
90
|
+
reclaiming.set(entry, run);
|
|
91
|
+
return run;
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* The row an event names.
|
|
95
|
+
*
|
|
96
|
+
* The loader hands the emitting entry as the event's `this`, but cordis does
|
|
97
|
+
* not rebind `this` for listeners added through `ctx.on`, so a listener can
|
|
98
|
+
* also receive the payload as its first argument. Both shapes are accepted;
|
|
99
|
+
* the real Host always provides one of them.
|
|
100
|
+
*/
|
|
101
|
+
const entryOfEvent = (self, first) => {
|
|
102
|
+
const candidate = self ?? first;
|
|
103
|
+
return candidate !== null && typeof candidate === "object" && "options" in candidate ? candidate : void 0;
|
|
104
|
+
};
|
|
105
|
+
const listeners = [];
|
|
106
|
+
const listen = (event, listener) => {
|
|
107
|
+
const dispose = ctx.on?.(event, listener);
|
|
108
|
+
if (dispose !== void 0) listeners.push(dispose);
|
|
109
|
+
};
|
|
110
|
+
listen("loader/patch-context", function(first, second) {
|
|
111
|
+
const nextFn = typeof first === "function" ? first : second;
|
|
112
|
+
const entry = entryOfEvent(this, typeof first === "function" ? void 0 : first);
|
|
113
|
+
if (entry === void 0 || typeof nextFn !== "function" || !isOfficialAutoReviewRow(entry) || handled.has(entry)) return nextFn === void 0 ? void 0 : nextFn();
|
|
114
|
+
handled.add(entry);
|
|
115
|
+
seam.yieldRegistration();
|
|
116
|
+
reclaim(entry);
|
|
117
|
+
return nextFn();
|
|
118
|
+
});
|
|
119
|
+
listen("loader/partial-dispose", function(first) {
|
|
120
|
+
const entry = entryOfEvent(this, first);
|
|
121
|
+
if (entry === void 0 || !isOfficialAutoReviewRow(entry)) return;
|
|
122
|
+
handled.delete(entry);
|
|
123
|
+
seam.claim();
|
|
124
|
+
});
|
|
125
|
+
const loader = ctx.get("loader", false);
|
|
126
|
+
if (loader !== void 0 && typeof loader.entries === "function") for (const candidate of loader.entries()) {
|
|
127
|
+
if (!isOfficialAutoReviewRow(candidate)) continue;
|
|
128
|
+
if (alreadyDisabled(candidate)) continue;
|
|
129
|
+
reclaim(candidate);
|
|
130
|
+
}
|
|
131
|
+
const disposable = () => {
|
|
132
|
+
for (const dispose of listeners.splice(0)) dispose();
|
|
133
|
+
};
|
|
134
|
+
const effect = ctx.effect;
|
|
135
|
+
if (effect !== void 0) {
|
|
136
|
+
const registration = effect(function* () {
|
|
137
|
+
yield disposable;
|
|
138
|
+
}, "model-switch: auto-review takeover");
|
|
139
|
+
if (typeof registration === "function") listeners.push(registration);
|
|
140
|
+
}
|
|
141
|
+
return reclaim;
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
3
144
|
//#region lib/types/auto-review.js
|
|
4
145
|
/**
|
|
5
146
|
* Model Switch-owned Auto reviewer row.
|
|
@@ -24,9 +165,14 @@ import { AsyncLocalStorage } from "node:async_hooks";
|
|
|
24
165
|
* interference when multiple sessions review tool calls concurrently.
|
|
25
166
|
*
|
|
26
167
|
* 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
|
-
*
|
|
168
|
+
* the profile actually started with the experimental-auto-review bundle. Two
|
|
169
|
+
* rows cannot share the seam — `permissionPresets.registerAuto` is a single
|
|
170
|
+
* registration and the official probe is unshielded — so this row also
|
|
171
|
+
* **takes the seam over** (see `./auto-review-takeover.ts`): a composed or
|
|
172
|
+
* re-enabled official row is displaced through the released loader seams, the
|
|
173
|
+
* registration is handed over and reclaimed around that displacement, and the
|
|
174
|
+
* gate this row mounts keeps the defensive `permissionPresets` shadow so a
|
|
175
|
+
* degraded preset service can never fail a tool call again.
|
|
30
176
|
*
|
|
31
177
|
* @module dsh-model-switch/auto-review
|
|
32
178
|
*/
|
|
@@ -41,11 +187,18 @@ const name = "model-switch-auto-review";
|
|
|
41
187
|
* every tool call fail with `cannot get property "workingDirectory" without
|
|
42
188
|
* inject`. The gate is now mounted as its own plugin (see {@link autoReviewGate}),
|
|
43
189
|
* so cordis resolves the official dependency list — including future additions —
|
|
44
|
-
* and this row declares only what it reads itself
|
|
190
|
+
* and this row declares only what it reads itself (`loader` drives the
|
|
191
|
+
* takeover).
|
|
45
192
|
*/
|
|
46
|
-
const inject = [
|
|
193
|
+
const inject = [
|
|
194
|
+
"llm",
|
|
195
|
+
"modelSwitch",
|
|
196
|
+
"loader"
|
|
197
|
+
];
|
|
47
198
|
/** Optional bundle whose composition opts a profile into Auto review. */
|
|
48
199
|
const AUTO_REVIEW_BUNDLE = "@deepseek-ai/dsh-experimental-auto-review";
|
|
200
|
+
/** The Auto preset name; `registerAuto` is what publishes it. */
|
|
201
|
+
const AUTO_PRESET = "auto";
|
|
49
202
|
/**
|
|
50
203
|
* Resolve the pinned reviewer route from live settings.
|
|
51
204
|
* @param settings - Model Switch settings carrying the optional reviewer pair.
|
|
@@ -119,19 +272,6 @@ function bindAutoReviewSessionCorrelation(scope, activeSession) {
|
|
|
119
272
|
yield scope.on("tools/pre-execute", (exec, next) => activeSession.run(exec.agent?.session, next), { prepend: true });
|
|
120
273
|
}, "model-switch: auto-review session correlation");
|
|
121
274
|
}
|
|
122
|
-
/**
|
|
123
|
-
* Build the scoped context the official gate runs under.
|
|
124
|
-
* @param ctx - host context whose modelSwitch settings pin the reviewer route.
|
|
125
|
-
* @param activeSession - optional AsyncLocalStorage correlating each `tools/pre-execute` call to its session.
|
|
126
|
-
* @returns A child context shadowing the llm service and, when the permission
|
|
127
|
-
* preset service is reachable, that service too.
|
|
128
|
-
*/
|
|
129
|
-
function reviewScope(ctx, activeSession = new AsyncLocalStorage()) {
|
|
130
|
-
const permissionPresets = permissionPresetsOf(ctx);
|
|
131
|
-
const shadows = { llm: reviewLlm(ctx.llm, () => resolveSessionAutoReviewRoute(ctx, activeSession.getStore())) };
|
|
132
|
-
if (permissionPresets !== void 0) shadows.permissionPresets = defensivePermissionPresets(permissionPresets, ctx.logger);
|
|
133
|
-
return ctx.extend(shadows);
|
|
134
|
-
}
|
|
135
275
|
/** The preset name the service reports when no configured preset matches. */
|
|
136
276
|
const CUSTOM_PRESET = "custom";
|
|
137
277
|
/** Read the live permission-preset service without requiring an injection. */
|
|
@@ -144,34 +284,69 @@ function permissionPresetsOf(ctx) {
|
|
|
144
284
|
}
|
|
145
285
|
}
|
|
146
286
|
/**
|
|
287
|
+
* A compact dump of the fiber chain a service resolution walks.
|
|
288
|
+
*
|
|
289
|
+
* `cannot get required service "sessionProjections" in inactive context` is
|
|
290
|
+
* thrown by the first fiber in the caller's chain that declares the service
|
|
291
|
+
* without a resolved snapshot; the chain names that fiber directly.
|
|
292
|
+
* @param ctx - the context whose chain is walked.
|
|
293
|
+
* @returns One `uid/state/name/inject` segment per fiber, outermost last.
|
|
294
|
+
*/
|
|
295
|
+
function fiberChainOf(ctx) {
|
|
296
|
+
const parts = [];
|
|
297
|
+
let node = ctx;
|
|
298
|
+
for (let step = 0; step < 8; step += 1) {
|
|
299
|
+
const fiber = node?.fiber;
|
|
300
|
+
if (fiber === void 0) break;
|
|
301
|
+
const injects = Object.keys(fiber.inject ?? {}).join("|");
|
|
302
|
+
parts.push(`uid=${String(fiber.uid)} state=${String(fiber.state)} name=${String(fiber.name)} inject=[${injects}]`);
|
|
303
|
+
node = fiber.parent;
|
|
304
|
+
}
|
|
305
|
+
return parts.length === 0 ? "no fiber chain" : parts.join(" -> ");
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
147
308
|
* Wrap the permission-preset service so the official gate's per-call probe can
|
|
148
|
-
* never fail a tool call.
|
|
309
|
+
* never fail a tool call, and broker its single Auto registration.
|
|
149
310
|
*
|
|
150
311
|
* DSH's Auto gate starts every `tools/pre-execute` with
|
|
151
312
|
* `permissionPresets.current(session)`, and that method resolves the presets
|
|
152
313
|
* service's own `sessionProjections` injection. When the presets fiber has
|
|
153
314
|
* turned inactive the probe throws `cannot get required service
|
|
154
315
|
* "sessionProjections" in inactive context` outside the gate's reviewer error
|
|
155
|
-
* wrapper, so every tool call dies. This wrapper
|
|
156
|
-
*
|
|
157
|
-
*
|
|
158
|
-
*
|
|
316
|
+
* wrapper, so every tool call dies. This wrapper resolves the service lazily
|
|
317
|
+
* (so the shadow is in place even while the service is still activating),
|
|
318
|
+
* degrades the probe to `custom` (the gate then skips review and calls
|
|
319
|
+
* `next()`), and reports the lost capability once — with the fiber chain that
|
|
320
|
+
* failed resolution, so the next occurrence names the inactive fiber.
|
|
321
|
+
*
|
|
322
|
+
* `registerAuto` is **brokered**: the gate yields the disposer into its own
|
|
323
|
+
* effect, but the takeover must be able to hand the single registration to a
|
|
324
|
+
* displacing row and take it back without unmounting the gate (the gate's
|
|
325
|
+
* disposal would move live `auto` sessions to `danger-full-access`).
|
|
159
326
|
*
|
|
160
|
-
* @param
|
|
327
|
+
* @param resolve - resolves the host's permission-preset service per call.
|
|
161
328
|
* @param log - the row logger used for the single degradation report.
|
|
162
|
-
* @
|
|
329
|
+
* @param chain - diagnostic dump of the resolution chain; defaults to empty.
|
|
330
|
+
* @returns A managed preset face.
|
|
163
331
|
*/
|
|
164
|
-
function defensivePermissionPresets(
|
|
332
|
+
function defensivePermissionPresets(resolve, log, chain = () => "") {
|
|
165
333
|
let reported = false;
|
|
334
|
+
let admit;
|
|
335
|
+
let release;
|
|
166
336
|
const degrade = (operation, error) => {
|
|
167
337
|
if (reported) return;
|
|
168
338
|
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`);
|
|
339
|
+
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()}]`);
|
|
340
|
+
};
|
|
341
|
+
const requireService = () => {
|
|
342
|
+
const face = resolve();
|
|
343
|
+
if (face === void 0) throw new Error("permission: the permission-preset service is unavailable");
|
|
344
|
+
return face;
|
|
170
345
|
};
|
|
171
346
|
return {
|
|
172
347
|
current(session) {
|
|
173
348
|
try {
|
|
174
|
-
return
|
|
349
|
+
return requireService().current(session);
|
|
175
350
|
} catch (error) {
|
|
176
351
|
degrade("current()", error);
|
|
177
352
|
return CUSTOM_PRESET;
|
|
@@ -179,22 +354,56 @@ function defensivePermissionPresets(real, log) {
|
|
|
179
354
|
},
|
|
180
355
|
set(session, preset) {
|
|
181
356
|
try {
|
|
182
|
-
return
|
|
357
|
+
return requireService().set(session, preset);
|
|
183
358
|
} catch (error) {
|
|
184
359
|
degrade("set()", error);
|
|
185
360
|
return;
|
|
186
361
|
}
|
|
187
362
|
},
|
|
188
|
-
registerAuto(
|
|
363
|
+
registerAuto(next) {
|
|
364
|
+
admit = next;
|
|
365
|
+
release = requireService().registerAuto(next);
|
|
366
|
+
return () => {
|
|
367
|
+
admit = void 0;
|
|
368
|
+
const dispose = release;
|
|
369
|
+
release = void 0;
|
|
370
|
+
return dispose?.();
|
|
371
|
+
};
|
|
372
|
+
},
|
|
373
|
+
releaseForTakeover() {
|
|
374
|
+
const dispose = release;
|
|
375
|
+
release = void 0;
|
|
376
|
+
dispose?.();
|
|
377
|
+
},
|
|
378
|
+
reclaimRegistration() {
|
|
379
|
+
if (release !== void 0) return true;
|
|
380
|
+
if (admit === void 0) return false;
|
|
189
381
|
try {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return
|
|
382
|
+
release = requireService().registerAuto(admit);
|
|
383
|
+
return true;
|
|
384
|
+
} catch {
|
|
385
|
+
return false;
|
|
194
386
|
}
|
|
387
|
+
},
|
|
388
|
+
get registered() {
|
|
389
|
+
return release !== void 0;
|
|
195
390
|
}
|
|
196
391
|
};
|
|
197
392
|
}
|
|
393
|
+
/**
|
|
394
|
+
* Build the scoped context the official gate runs under.
|
|
395
|
+
* @param ctx - host context whose modelSwitch settings pin the reviewer route.
|
|
396
|
+
* @param activeSession - optional AsyncLocalStorage correlating each `tools/pre-execute` call to its session.
|
|
397
|
+
* @param presets - optional managed preset face shared with the row's takeover.
|
|
398
|
+
* @returns A child context shadowing the llm service and the permission-preset
|
|
399
|
+
* service (always: a gate that sees the raw service can fail a tool call).
|
|
400
|
+
*/
|
|
401
|
+
function reviewScope(ctx, activeSession = new AsyncLocalStorage(), presets = defensivePermissionPresets(() => permissionPresetsOf(ctx), ctx.logger, () => fiberChainOf(ctx))) {
|
|
402
|
+
return ctx.extend({
|
|
403
|
+
llm: reviewLlm(ctx.llm, () => resolveSessionAutoReviewRoute(ctx, activeSession.getStore())),
|
|
404
|
+
permissionPresets: presets
|
|
405
|
+
});
|
|
406
|
+
}
|
|
198
407
|
/** Structural read of launcher-provided profile facts (the app-boot seam). */
|
|
199
408
|
function autoReviewBundleActive(ctx) {
|
|
200
409
|
let profile;
|
|
@@ -207,6 +416,15 @@ function autoReviewBundleActive(ctx) {
|
|
|
207
416
|
const bundles = profile.startedBundles;
|
|
208
417
|
return Array.isArray(bundles) && bundles.includes(AUTO_REVIEW_BUNDLE);
|
|
209
418
|
}
|
|
419
|
+
/** Structural read of the loader service the takeover drives. */
|
|
420
|
+
function loaderOf(ctx) {
|
|
421
|
+
try {
|
|
422
|
+
const loader = ctx.get("loader", false);
|
|
423
|
+
return loader !== null && typeof loader === "object" ? loader : void 0;
|
|
424
|
+
} catch {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
210
428
|
/**
|
|
211
429
|
* Wrap the official gate as a plugin that declares its own dependencies.
|
|
212
430
|
*
|
|
@@ -247,11 +465,14 @@ function autoReviewGate(gate, hooks = {}) {
|
|
|
247
465
|
*/
|
|
248
466
|
let lastGateFiber;
|
|
249
467
|
/**
|
|
250
|
-
* Mount the official gate under the reviewer-route scope.
|
|
468
|
+
* Mount the official gate under the reviewer-route scope and own the seam.
|
|
251
469
|
*
|
|
252
470
|
* A profile that never enabled the optional bundle keeps no Auto preset and no
|
|
253
471
|
* reviewer; a deployment missing the official package reports the lost
|
|
254
|
-
* capability explicitly instead of guessing another path.
|
|
472
|
+
* capability explicitly instead of guessing another path. A composed (or
|
|
473
|
+
* plugin-manager re-enabled) official row is displaced first — see
|
|
474
|
+
* `./auto-review-takeover.ts` for the choreography and why the yield is
|
|
475
|
+
* synchronous.
|
|
255
476
|
*/
|
|
256
477
|
async function apply(ctx) {
|
|
257
478
|
if (!autoReviewBundleActive(ctx)) {
|
|
@@ -266,42 +487,105 @@ async function apply(ctx) {
|
|
|
266
487
|
ctx.logger.error(cause);
|
|
267
488
|
return;
|
|
268
489
|
}
|
|
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
|
-
}
|
|
490
|
+
const presets = defensivePermissionPresets(() => permissionPresetsOf(ctx), ctx.logger, () => fiberChainOf(ctx));
|
|
277
491
|
const activeSession = new AsyncLocalStorage();
|
|
278
|
-
const scope = reviewScope(ctx, activeSession);
|
|
279
|
-
let
|
|
280
|
-
let
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
492
|
+
const scope = reviewScope(ctx, activeSession, presets);
|
|
493
|
+
let gateReady = false;
|
|
494
|
+
let mountInFlight;
|
|
495
|
+
/** Sessions currently on the Auto preset (best-effort). */
|
|
496
|
+
const rememberAuto = () => {
|
|
497
|
+
const remembered = [];
|
|
498
|
+
try {
|
|
499
|
+
const sessions = ctx.get("sessions", false);
|
|
500
|
+
for (const session of sessions?.list?.() ?? []) if (presets.current(session) === AUTO_PRESET) remembered.push(session);
|
|
501
|
+
} catch {}
|
|
502
|
+
return remembered;
|
|
503
|
+
};
|
|
504
|
+
const restoreAuto = (remembered) => {
|
|
505
|
+
for (const session of remembered) try {
|
|
506
|
+
presets.set(session, AUTO_PRESET);
|
|
507
|
+
} catch {}
|
|
508
|
+
};
|
|
509
|
+
/**
|
|
510
|
+
* Run one displacement step without losing the sessions' Auto selection.
|
|
511
|
+
*
|
|
512
|
+
* Disposing a gate moves live `auto` sessions to `danger-full-access` by
|
|
513
|
+
* design (the official teardown runs on both the displaced row and this
|
|
514
|
+
* row's own re-mounts); the remembered selection is restored afterwards.
|
|
515
|
+
*/
|
|
516
|
+
const preserveAuto = async (action) => {
|
|
517
|
+
const remembered = rememberAuto();
|
|
518
|
+
await action();
|
|
519
|
+
restoreAuto(remembered);
|
|
520
|
+
};
|
|
521
|
+
/** Mount this row's shielded gate exactly once; failures dispose and report. */
|
|
522
|
+
const mountGate = () => {
|
|
523
|
+
mountInFlight ??= (async () => {
|
|
524
|
+
let outcome = "failed";
|
|
525
|
+
await preserveAuto(async () => {
|
|
526
|
+
const previous = lastGateFiber;
|
|
527
|
+
lastGateFiber = void 0;
|
|
528
|
+
gateReady = false;
|
|
529
|
+
if (previous !== void 0) try {
|
|
530
|
+
await previous.dispose();
|
|
531
|
+
} catch (cause) {
|
|
532
|
+
ctx.logger.info("model-switch: the previous auto-review gate had already unloaded; continuing with the new one");
|
|
533
|
+
ctx.logger.info(String(cause));
|
|
534
|
+
}
|
|
535
|
+
let failure;
|
|
536
|
+
let applied = false;
|
|
537
|
+
const fiber = scope.plugin(autoReviewGate(official, {
|
|
538
|
+
onApplied: () => {
|
|
539
|
+
applied = true;
|
|
540
|
+
bindAutoReviewSessionCorrelation(scope, activeSession);
|
|
541
|
+
},
|
|
542
|
+
onFailure: (error) => {
|
|
543
|
+
failure = error;
|
|
544
|
+
}
|
|
545
|
+
}));
|
|
546
|
+
try {
|
|
547
|
+
await fiber;
|
|
548
|
+
} catch (cause) {
|
|
549
|
+
failure ??= cause;
|
|
550
|
+
}
|
|
551
|
+
if (failure !== void 0) {
|
|
552
|
+
try {
|
|
553
|
+
await fiber.dispose();
|
|
554
|
+
} catch {}
|
|
555
|
+
ctx.logger.warn(`model-switch: the Auto reviewer gate did not mount (${String(failure)}); the displaced registration may still be releasing`);
|
|
556
|
+
outcome = "failed";
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (!applied) {
|
|
560
|
+
ctx.logger.info("model-switch: the official auto-review gate is waiting for its host services; the reviewer route override is not bound yet");
|
|
561
|
+
outcome = "waiting";
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
lastGateFiber = fiber;
|
|
565
|
+
gateReady = true;
|
|
566
|
+
outcome = "applied";
|
|
567
|
+
});
|
|
568
|
+
return outcome;
|
|
569
|
+
})().finally(() => {
|
|
570
|
+
mountInFlight = void 0;
|
|
571
|
+
});
|
|
572
|
+
return mountInFlight;
|
|
573
|
+
};
|
|
574
|
+
/** Make this row's Auto registration live; true once it is held. */
|
|
575
|
+
const claim = async () => {
|
|
576
|
+
if (presets.registered) return true;
|
|
577
|
+
if (gateReady) return presets.reclaimRegistration();
|
|
578
|
+
return await mountGate() !== "failed";
|
|
579
|
+
};
|
|
580
|
+
const reclaim = installAutoReviewTakeover(ctx, {
|
|
581
|
+
yieldRegistration: () => presets.releaseForTakeover(),
|
|
582
|
+
claim,
|
|
583
|
+
preserveAuto
|
|
584
|
+
});
|
|
585
|
+
const entry = officialAutoReviewEntry(loaderOf(ctx));
|
|
586
|
+
if (entry !== void 0 && entry.disabled !== true) await reclaim(entry);
|
|
587
|
+
else await claim();
|
|
588
|
+
if (!presets.registered) ctx.logger.error("model-switch: the Auto registration is not held by this row; Auto review runs without the configurable reviewer route");
|
|
305
589
|
}
|
|
306
590
|
//#endregion
|
|
307
591
|
export { apply, autoReviewGate, autoReviewRouteOf, bindAutoReviewSessionCorrelation, defensivePermissionPresets, inject, 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,90 @@
|
|
|
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 = 20;
|
|
40
|
+
/** Delay between takeover attempts; the first attempt is immediate. */
|
|
41
|
+
export declare const TAKEOVER_RETRY_DELAY_MS = 10;
|
|
42
|
+
/** The seam this row's gate owns; the takeover drives it around the official row. */
|
|
43
|
+
export interface AutoReviewSeam {
|
|
44
|
+
/**
|
|
45
|
+
* Synchronously free the Auto registration so a displacing row can register
|
|
46
|
+
* cleanly. Keeping the admit callback is the seam's business; claiming puts
|
|
47
|
+
* the registration back.
|
|
48
|
+
*/
|
|
49
|
+
yieldRegistration(): void;
|
|
50
|
+
/** Make this row's Auto registration live; true once it is held. */
|
|
51
|
+
claim(): Promise<boolean>;
|
|
52
|
+
/** Run one takeover step without losing the sessions' Auto selection. */
|
|
53
|
+
preserveAuto(action: () => Promise<void>): Promise<void>;
|
|
54
|
+
}
|
|
55
|
+
/** Whether one loader entry is the official Auto-review row (strict package identity). */
|
|
56
|
+
export declare function isOfficialAutoReviewRow(entry: Entry | undefined): boolean;
|
|
57
|
+
/** The first composed official Auto-review row, if the profile declares one. */
|
|
58
|
+
export declare function officialAutoReviewEntry(loader: Loader | undefined): Entry | undefined;
|
|
59
|
+
/**
|
|
60
|
+
* Displace the official Auto-review row and take the registration back.
|
|
61
|
+
*
|
|
62
|
+
* The official row is disabled through the public `Entry.update` (which starts
|
|
63
|
+
* its fiber's disposal — the loader does not await it, so the claim retries
|
|
64
|
+
* until the displaced registration actually released). The Auto selection of
|
|
65
|
+
* live sessions is preserved around the displacement: the official gate's
|
|
66
|
+
* disposal moves `auto` sessions to `danger-full-access` by design, and this
|
|
67
|
+
* row's registration puts them back.
|
|
68
|
+
*
|
|
69
|
+
* @param ctx - this row's context, used only for logging.
|
|
70
|
+
* @param entry - the official row to displace.
|
|
71
|
+
* @param seam - the Auto-registration seam to yield and reclaim.
|
|
72
|
+
* @returns a promise settling once the takeover attempt loop finished.
|
|
73
|
+
*/
|
|
74
|
+
export declare function reclaimOfficialAutoReviewRow(ctx: Context, entry: Entry, seam: AutoReviewSeam): Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Install the takeover choreography for an official row that activates later.
|
|
77
|
+
*
|
|
78
|
+
* The plugin manager can re-enable the official row at any time (it writes
|
|
79
|
+
* `disabled: false` into the profile patch layer, which applies after every
|
|
80
|
+
* bundle layer). The `loader/patch-context` listener yields this row's
|
|
81
|
+
* registration **synchronously** before the entry's plugin `apply` runs, so the
|
|
82
|
+
* official row applies cleanly instead of colliding; the reclaim then disables
|
|
83
|
+
* it and takes the registration back.
|
|
84
|
+
*
|
|
85
|
+
* @param ctx - this row's context.
|
|
86
|
+
* @param seam - the Auto-registration seam to yield and reclaim.
|
|
87
|
+
* @returns the guarded reclaim function, so the caller's startup takeover and
|
|
88
|
+
* the listeners share one in-flight guard per entry.
|
|
89
|
+
*/
|
|
90
|
+
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. */
|
|
@@ -96,41 +102,61 @@ export declare function resolveSessionAutoReviewRoute(ctx: Context, session?: Se
|
|
|
96
102
|
* @param activeSession - AsyncLocalStorage holding the session under review.
|
|
97
103
|
*/
|
|
98
104
|
export declare function bindAutoReviewSessionCorrelation(scope: Context, activeSession: AsyncLocalStorage<Session | undefined>): void;
|
|
99
|
-
/**
|
|
100
|
-
* Build the scoped context the official gate runs under.
|
|
101
|
-
* @param ctx - host context whose modelSwitch settings pin the reviewer route.
|
|
102
|
-
* @param activeSession - optional AsyncLocalStorage correlating each `tools/pre-execute` call to its session.
|
|
103
|
-
* @returns A child context shadowing the llm service and, when the permission
|
|
104
|
-
* preset service is reachable, that service too.
|
|
105
|
-
*/
|
|
106
|
-
export declare function reviewScope(ctx: Context, activeSession?: AsyncLocalStorage<Session | undefined>): Context;
|
|
107
105
|
/** The permission-preset face the official gate reads; structural so a missing package never breaks a build. */
|
|
108
106
|
export interface PermissionPresetsFace {
|
|
109
107
|
current(session: Session): string;
|
|
110
108
|
set(session: Session, preset: string): unknown;
|
|
111
109
|
registerAuto(admit: () => void): unknown;
|
|
112
110
|
}
|
|
111
|
+
/**
|
|
112
|
+
* The managed face: what the gate sees, plus the registration handle the
|
|
113
|
+
* takeover drives.
|
|
114
|
+
*/
|
|
115
|
+
export interface ManagedPermissionPresetsFace extends PermissionPresetsFace {
|
|
116
|
+
/** Synchronously release the live Auto registration, keeping the admit callback. */
|
|
117
|
+
releaseForTakeover(): void;
|
|
118
|
+
/** Take the Auto registration back; true once it is live. */
|
|
119
|
+
reclaimRegistration(): boolean;
|
|
120
|
+
/** Whether this face currently holds the Auto registration. */
|
|
121
|
+
readonly registered: boolean;
|
|
122
|
+
}
|
|
113
123
|
/**
|
|
114
124
|
* Wrap the permission-preset service so the official gate's per-call probe can
|
|
115
|
-
* never fail a tool call.
|
|
125
|
+
* never fail a tool call, and broker its single Auto registration.
|
|
116
126
|
*
|
|
117
127
|
* DSH's Auto gate starts every `tools/pre-execute` with
|
|
118
128
|
* `permissionPresets.current(session)`, and that method resolves the presets
|
|
119
129
|
* service's own `sessionProjections` injection. When the presets fiber has
|
|
120
130
|
* turned inactive the probe throws `cannot get required service
|
|
121
131
|
* "sessionProjections" in inactive context` outside the gate's reviewer error
|
|
122
|
-
* wrapper, so every tool call dies. This wrapper
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
132
|
+
* wrapper, so every tool call dies. This wrapper resolves the service lazily
|
|
133
|
+
* (so the shadow is in place even while the service is still activating),
|
|
134
|
+
* degrades the probe to `custom` (the gate then skips review and calls
|
|
135
|
+
* `next()`), and reports the lost capability once — with the fiber chain that
|
|
136
|
+
* failed resolution, so the next occurrence names the inactive fiber.
|
|
137
|
+
*
|
|
138
|
+
* `registerAuto` is **brokered**: the gate yields the disposer into its own
|
|
139
|
+
* effect, but the takeover must be able to hand the single registration to a
|
|
140
|
+
* displacing row and take it back without unmounting the gate (the gate's
|
|
141
|
+
* disposal would move live `auto` sessions to `danger-full-access`).
|
|
142
|
+
*
|
|
143
|
+
* @param resolve - resolves the host's permission-preset service per call.
|
|
128
144
|
* @param log - the row logger used for the single degradation report.
|
|
129
|
-
* @
|
|
145
|
+
* @param chain - diagnostic dump of the resolution chain; defaults to empty.
|
|
146
|
+
* @returns A managed preset face.
|
|
130
147
|
*/
|
|
131
|
-
export declare function defensivePermissionPresets(
|
|
148
|
+
export declare function defensivePermissionPresets(resolve: () => PermissionPresetsFace | undefined, log: {
|
|
132
149
|
warn(line: string): void;
|
|
133
|
-
}):
|
|
150
|
+
}, chain?: () => string): ManagedPermissionPresetsFace;
|
|
151
|
+
/**
|
|
152
|
+
* Build the scoped context the official gate runs under.
|
|
153
|
+
* @param ctx - host context whose modelSwitch settings pin the reviewer route.
|
|
154
|
+
* @param activeSession - optional AsyncLocalStorage correlating each `tools/pre-execute` call to its session.
|
|
155
|
+
* @param presets - optional managed preset face shared with the row's takeover.
|
|
156
|
+
* @returns A child context shadowing the llm service and the permission-preset
|
|
157
|
+
* service (always: a gate that sees the raw service can fail a tool call).
|
|
158
|
+
*/
|
|
159
|
+
export declare function reviewScope(ctx: Context, activeSession?: AsyncLocalStorage<Session | undefined>, presets?: ManagedPermissionPresetsFace): Context;
|
|
134
160
|
/**
|
|
135
161
|
* Structural view of the official gate's plugin face.
|
|
136
162
|
*
|
|
@@ -168,10 +194,13 @@ export declare function autoReviewGate(gate: OfficialAutoReviewGate, hooks?: {
|
|
|
168
194
|
readonly onFailure?: (error: unknown) => void;
|
|
169
195
|
}): MountedAutoReviewGate;
|
|
170
196
|
/**
|
|
171
|
-
* Mount the official gate under the reviewer-route scope.
|
|
197
|
+
* Mount the official gate under the reviewer-route scope and own the seam.
|
|
172
198
|
*
|
|
173
199
|
* A profile that never enabled the optional bundle keeps no Auto preset and no
|
|
174
200
|
* reviewer; a deployment missing the official package reports the lost
|
|
175
|
-
* capability explicitly instead of guessing another path.
|
|
201
|
+
* capability explicitly instead of guessing another path. A composed (or
|
|
202
|
+
* plugin-manager re-enabled) official row is displaced first — see
|
|
203
|
+
* `./auto-review-takeover.ts` for the choreography and why the yield is
|
|
204
|
+
* synchronous.
|
|
176
205
|
*/
|
|
177
206
|
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 与官方自动压缩)指定摘要模型;关闭(两个字段都留空)时使用官方默认,即当前会话的目标模型。";
|