dsh-model-switch 0.4.31 → 0.4.33
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 +18 -0
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/lib/auto-review-BLtwr83V.js +738 -0
- package/lib/auto-review.js +2 -591
- package/lib/client.js +5 -2
- package/lib/compaction.js +1 -1
- package/lib/index.js +8 -2
- package/lib/session-route-D1zoZgf3.js +258 -0
- package/lib/session-title.js +1 -1
- package/lib/subagent-runtime.js +1 -1
- package/lib/types/auto-review-takeover.d.ts +12 -2
- package/lib/types/auto-review.d.ts +48 -0
- package/lib/types/client/locales.d.ts +1 -0
- package/lib/types/host-runtime.d.ts +7 -0
- package/lib/types/runtime-capabilities.d.ts +12 -1
- package/package.json +1 -1
package/lib/auto-review.js
CHANGED
|
@@ -1,591 +1,2 @@
|
|
|
1
|
-
import { i as
|
|
2
|
-
|
|
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
|
|
144
|
-
//#region lib/types/auto-review.js
|
|
145
|
-
/**
|
|
146
|
-
* Model Switch-owned Auto reviewer row.
|
|
147
|
-
*
|
|
148
|
-
* The official `@deepseek-ai/dsh-experimental-auto-review` gate hardwires its
|
|
149
|
-
* reviewer to the session's own request route (`header.config`). This row runs
|
|
150
|
-
* the official `apply` under a scoped child context whose `llm` service is
|
|
151
|
-
* shadowed by a wrapper that rewrites only `stream()`'s provider/model — and
|
|
152
|
-
* only when the reviewing session's derived route (its provider group's
|
|
153
|
-
* reviewer pair, else the deployment-wide reviewer card) pins one. With no
|
|
154
|
-
* route pinned the wrapper passes options through by identity, so the reviewer
|
|
155
|
-
* follows the session model exactly.
|
|
156
|
-
*
|
|
157
|
-
* Although the official gate omits `sessionId` and `purpose` from its frozen
|
|
158
|
-
* `GenerateOptions`, its `tools/pre-execute` waterfall listener receives
|
|
159
|
-
* `exec.agent` (`exec.agent.session`). After running `official.apply(scope)`,
|
|
160
|
-
* this module registers a `{ prepend: true }` `tools/pre-execute` listener on
|
|
161
|
-
* the same scope — which Cordis places ahead of the official gate's listener in
|
|
162
|
-
* the waterfall — and binds `exec.agent?.session` in an `AsyncLocalStorage`
|
|
163
|
-
* scope around `next()`. Every `scope.llm.stream()` call from `classifyRisk`
|
|
164
|
-
* therefore resolves the exact session being reviewed, with zero cross-session
|
|
165
|
-
* interference when multiple sessions review tool calls concurrently.
|
|
166
|
-
*
|
|
167
|
-
* The official gate stays an opt-in capability: the row mounts it only when
|
|
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.
|
|
176
|
-
*
|
|
177
|
-
* @module dsh-model-switch/auto-review
|
|
178
|
-
*/
|
|
179
|
-
/** Cordis plugin name used by loader diagnostics. */
|
|
180
|
-
const name = "model-switch-auto-review";
|
|
181
|
-
/**
|
|
182
|
-
* Only the services this row's own code touches.
|
|
183
|
-
*
|
|
184
|
-
* The official gate used to run inside this row's fiber, so this list had to
|
|
185
|
-
* mirror every service the official `apply` reads. DSH 0.2.1 added
|
|
186
|
-
* `workingDirectory` to the official gate and the mirror went stale, which made
|
|
187
|
-
* every tool call fail with `cannot get property "workingDirectory" without
|
|
188
|
-
* inject`. The gate is now mounted as its own plugin (see {@link autoReviewGate}),
|
|
189
|
-
* so cordis resolves the official dependency list — including future additions —
|
|
190
|
-
* and this row declares only what it reads itself (`loader` drives the
|
|
191
|
-
* takeover).
|
|
192
|
-
*/
|
|
193
|
-
const inject = [
|
|
194
|
-
"llm",
|
|
195
|
-
"modelSwitch",
|
|
196
|
-
"loader"
|
|
197
|
-
];
|
|
198
|
-
/** Optional bundle whose composition opts a profile into Auto review. */
|
|
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";
|
|
202
|
-
/**
|
|
203
|
-
* Resolve the pinned reviewer route from live settings.
|
|
204
|
-
* @param settings - Model Switch settings carrying the optional reviewer pair.
|
|
205
|
-
* @returns Both identifiers trimmed and non-empty, or undefined (follow session).
|
|
206
|
-
*/
|
|
207
|
-
function autoReviewRouteOf(settings) {
|
|
208
|
-
const provider = settings?.autoReviewProvider?.trim();
|
|
209
|
-
const model = settings?.autoReviewModel?.trim();
|
|
210
|
-
if (provider === void 0 || provider === "" || model === void 0 || model === "") return void 0;
|
|
211
|
-
return {
|
|
212
|
-
provider,
|
|
213
|
-
model
|
|
214
|
-
};
|
|
215
|
-
}
|
|
216
|
-
/**
|
|
217
|
-
* Wrap the real llm service so only `stream()` route fields are rewritten.
|
|
218
|
-
*
|
|
219
|
-
* Non-`stream` members pass through bound to the real service (Proxy reads
|
|
220
|
-
* would otherwise hand methods the proxy as `this`). The route is re-read per
|
|
221
|
-
* call so a settings change applies to the very next review without a reload.
|
|
222
|
-
*
|
|
223
|
-
* @param real - the host's `ctx.llm`.
|
|
224
|
-
* @param route - live route resolver; undefined means "do not rewrite".
|
|
225
|
-
* @returns A structurally compatible llm face for the scoped context.
|
|
226
|
-
*/
|
|
227
|
-
function reviewLlm(real, route) {
|
|
228
|
-
return new Proxy(real, { get(target, property, _receiver) {
|
|
229
|
-
if (property === "stream") return (options) => {
|
|
230
|
-
const pinned = route();
|
|
231
|
-
return pinned === void 0 ? target.stream(options) : target.stream({
|
|
232
|
-
...options,
|
|
233
|
-
provider: pinned.provider,
|
|
234
|
-
model: pinned.model
|
|
235
|
-
});
|
|
236
|
-
};
|
|
237
|
-
const value = Reflect.get(target, property, target);
|
|
238
|
-
return typeof value === "function" ? value.bind(target) : value;
|
|
239
|
-
} });
|
|
240
|
-
}
|
|
241
|
-
/**
|
|
242
|
-
* Resolve the reviewer route for one session (or deployment-wide when no session is bound).
|
|
243
|
-
*
|
|
244
|
-
* @param ctx - host context whose modelSwitch settings and sessionProjections derive the route.
|
|
245
|
-
* @param session - the session whose tool call is being reviewed, when known.
|
|
246
|
-
* @returns The derived reviewer route, or undefined to follow the session model.
|
|
247
|
-
*/
|
|
248
|
-
function resolveSessionAutoReviewRoute(ctx, session) {
|
|
249
|
-
let settings;
|
|
250
|
-
try {
|
|
251
|
-
settings = ctx.modelSwitch.currentSettings();
|
|
252
|
-
} catch {
|
|
253
|
-
settings = void 0;
|
|
254
|
-
}
|
|
255
|
-
const provider = sessionProviderOf(ctx, session);
|
|
256
|
-
return resolveCapabilityRoute(settings, provider, "autoReview");
|
|
257
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Install the outer `tools/pre-execute` correlation listener ahead of the official gate.
|
|
260
|
-
*
|
|
261
|
-
* Because Cordis `on(..., { prepend: true })` unshifts onto the event hook list,
|
|
262
|
-
* registering this listener AFTER `official.apply(scope)` places it at index 0
|
|
263
|
-
* — directly ahead of the official gate's own prepended listener — so
|
|
264
|
-
* `activeSession.run(exec.agent?.session, next)` wraps the entire official
|
|
265
|
-
* review execution.
|
|
266
|
-
*
|
|
267
|
-
* @param scope - the review scope context where the gate is mounted.
|
|
268
|
-
* @param activeSession - AsyncLocalStorage holding the session under review.
|
|
269
|
-
*/
|
|
270
|
-
function bindAutoReviewSessionCorrelation(scope, activeSession) {
|
|
271
|
-
scope.effect(function* () {
|
|
272
|
-
yield scope.on("tools/pre-execute", (exec, next) => activeSession.run(exec.agent?.session, next), { prepend: true });
|
|
273
|
-
}, "model-switch: auto-review session correlation");
|
|
274
|
-
}
|
|
275
|
-
/** The preset name the service reports when no configured preset matches. */
|
|
276
|
-
const CUSTOM_PRESET = "custom";
|
|
277
|
-
/** Read the live permission-preset service without requiring an injection. */
|
|
278
|
-
function permissionPresetsOf(ctx) {
|
|
279
|
-
try {
|
|
280
|
-
const face = ctx.get("permissionPresets", false);
|
|
281
|
-
return face !== null && typeof face === "object" ? face : void 0;
|
|
282
|
-
} catch {
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
}
|
|
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
|
-
/**
|
|
308
|
-
* Wrap the permission-preset service so the official gate's per-call probe can
|
|
309
|
-
* never fail a tool call, and broker its single Auto registration.
|
|
310
|
-
*
|
|
311
|
-
* DSH's Auto gate starts every `tools/pre-execute` with
|
|
312
|
-
* `permissionPresets.current(session)`, and that method resolves the presets
|
|
313
|
-
* service's own `sessionProjections` injection. When the presets fiber has
|
|
314
|
-
* turned inactive the probe throws `cannot get required service
|
|
315
|
-
* "sessionProjections" in inactive context` outside the gate's reviewer error
|
|
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`).
|
|
326
|
-
*
|
|
327
|
-
* @param resolve - resolves the host's permission-preset service per call.
|
|
328
|
-
* @param log - the row logger used for the single degradation report.
|
|
329
|
-
* @param chain - diagnostic dump of the resolution chain; defaults to empty.
|
|
330
|
-
* @returns A managed preset face.
|
|
331
|
-
*/
|
|
332
|
-
function defensivePermissionPresets(resolve, log, chain = () => "") {
|
|
333
|
-
let reported = false;
|
|
334
|
-
let admit;
|
|
335
|
-
let release;
|
|
336
|
-
const degrade = (operation, error) => {
|
|
337
|
-
if (reported) return;
|
|
338
|
-
reported = true;
|
|
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;
|
|
345
|
-
};
|
|
346
|
-
return {
|
|
347
|
-
current(session) {
|
|
348
|
-
try {
|
|
349
|
-
return requireService().current(session);
|
|
350
|
-
} catch (error) {
|
|
351
|
-
degrade("current()", error);
|
|
352
|
-
return CUSTOM_PRESET;
|
|
353
|
-
}
|
|
354
|
-
},
|
|
355
|
-
set(session, preset) {
|
|
356
|
-
try {
|
|
357
|
-
return requireService().set(session, preset);
|
|
358
|
-
} catch (error) {
|
|
359
|
-
degrade("set()", error);
|
|
360
|
-
return;
|
|
361
|
-
}
|
|
362
|
-
},
|
|
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;
|
|
381
|
-
try {
|
|
382
|
-
release = requireService().registerAuto(admit);
|
|
383
|
-
return true;
|
|
384
|
-
} catch {
|
|
385
|
-
return false;
|
|
386
|
-
}
|
|
387
|
-
},
|
|
388
|
-
get registered() {
|
|
389
|
-
return release !== void 0;
|
|
390
|
-
}
|
|
391
|
-
};
|
|
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
|
-
}
|
|
407
|
-
/** Structural read of launcher-provided profile facts (the app-boot seam). */
|
|
408
|
-
function autoReviewBundleActive(ctx) {
|
|
409
|
-
let profile;
|
|
410
|
-
try {
|
|
411
|
-
profile = ctx.get("profileContext", false);
|
|
412
|
-
} catch {
|
|
413
|
-
return false;
|
|
414
|
-
}
|
|
415
|
-
if (profile === null || typeof profile !== "object") return false;
|
|
416
|
-
const bundles = profile.startedBundles;
|
|
417
|
-
return Array.isArray(bundles) && bundles.includes(AUTO_REVIEW_BUNDLE);
|
|
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
|
-
}
|
|
428
|
-
/**
|
|
429
|
-
* Wrap the official gate as a plugin that declares its own dependencies.
|
|
430
|
-
*
|
|
431
|
-
* Directly calling `official.apply(scope)` ran the gate's code on this row's
|
|
432
|
-
* fiber, so every service the gate reads had to be mirrored in this row's
|
|
433
|
-
* `inject`. Mounting it instead lets cordis resolve the official list, which is
|
|
434
|
-
* what keeps a released Host's new dependency (0.2.1's `workingDirectory`) from
|
|
435
|
-
* breaking every tool call. The hooks let the caller observe whether the gate
|
|
436
|
-
* actually ran before it binds the session-correlation listener.
|
|
437
|
-
*
|
|
438
|
-
* @param gate - the official module (or an equivalent structural gate).
|
|
439
|
-
* @param hooks - applied/failure observers; the failure is rethrown.
|
|
440
|
-
* @returns A cordis plugin mounting the official gate unchanged.
|
|
441
|
-
*/
|
|
442
|
-
function autoReviewGate(gate, hooks = {}) {
|
|
443
|
-
return {
|
|
444
|
-
name: gate.name ?? "experimental-auto-review",
|
|
445
|
-
...gate.inject === void 0 ? {} : { inject: [...gate.inject] },
|
|
446
|
-
apply(gateCtx) {
|
|
447
|
-
try {
|
|
448
|
-
gate.apply(gateCtx);
|
|
449
|
-
} catch (error) {
|
|
450
|
-
hooks.onFailure?.(error);
|
|
451
|
-
throw error;
|
|
452
|
-
}
|
|
453
|
-
hooks.onApplied?.();
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
}
|
|
457
|
-
/**
|
|
458
|
-
* The gate fiber mounted by the current row instance.
|
|
459
|
-
*
|
|
460
|
-
* `registerAuto` is a single live registration owned by the gate's effect
|
|
461
|
-
* lifetime, and its disposer is what clears the Auto preset. A re-applied row
|
|
462
|
-
* must await the previous gate's unloading before the next registration runs,
|
|
463
|
-
* otherwise the new `registerAuto` throws `already registered` and its effect
|
|
464
|
-
* cleanup leaves the catalog without Auto.
|
|
465
|
-
*/
|
|
466
|
-
let lastGateFiber;
|
|
467
|
-
/**
|
|
468
|
-
* Mount the official gate under the reviewer-route scope and own the seam.
|
|
469
|
-
*
|
|
470
|
-
* A profile that never enabled the optional bundle keeps no Auto preset and no
|
|
471
|
-
* reviewer; a deployment missing the official package reports the lost
|
|
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.
|
|
476
|
-
*/
|
|
477
|
-
async function apply(ctx) {
|
|
478
|
-
if (!autoReviewBundleActive(ctx)) {
|
|
479
|
-
ctx.logger.info("model-switch: auto-review bundle is not active in this profile; the reviewer route override stays unmounted");
|
|
480
|
-
return;
|
|
481
|
-
}
|
|
482
|
-
let official;
|
|
483
|
-
try {
|
|
484
|
-
official = await import("@deepseek-ai/dsh-experimental-auto-review");
|
|
485
|
-
} catch (cause) {
|
|
486
|
-
ctx.logger.error("model-switch: @deepseek-ai/dsh-experimental-auto-review is unavailable; the Auto reviewer cannot run at all");
|
|
487
|
-
ctx.logger.error(cause);
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
const presets = defensivePermissionPresets(() => permissionPresetsOf(ctx), ctx.logger, () => fiberChainOf(ctx));
|
|
491
|
-
const activeSession = new AsyncLocalStorage();
|
|
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");
|
|
589
|
-
}
|
|
590
|
-
//#endregion
|
|
591
|
-
export { apply, autoReviewGate, autoReviewRouteOf, bindAutoReviewSessionCorrelation, defensivePermissionPresets, inject, name, resolveSessionAutoReviewRoute, reviewLlm, reviewScope };
|
|
1
|
+
import { a as bindAutoReviewSessionCorrelation, c as installToolCallSafetyNet, d as resolveSessionAutoReviewRoute, f as reviewLlm, i as autoReviewStatus, l as isReviewerInfrastructureFailure, n as autoReviewGate, o as defensivePermissionPresets, p as reviewScope, r as autoReviewRouteOf, s as inject, t as apply, u as name } from "./auto-review-BLtwr83V.js";
|
|
2
|
+
export { apply, autoReviewGate, autoReviewRouteOf, autoReviewStatus, bindAutoReviewSessionCorrelation, defensivePermissionPresets, inject, installToolCallSafetyNet, isReviewerInfrastructureFailure, name, resolveSessionAutoReviewRoute, reviewLlm, reviewScope };
|
package/lib/client.js
CHANGED
|
@@ -156,7 +156,8 @@ window.__ModuleLoader__.load({
|
|
|
156
156
|
imageProviderAdapters: Object.freeze({
|
|
157
157
|
available: true,
|
|
158
158
|
providers: Object.freeze(["codex", "grok"])
|
|
159
|
-
})
|
|
159
|
+
}),
|
|
160
|
+
autoReview: Object.freeze({ available: true })
|
|
160
161
|
});
|
|
161
162
|
//#endregion
|
|
162
163
|
//#region src/client/search-capabilities.ts
|
|
@@ -1802,7 +1803,7 @@ window.__ModuleLoader__.load({
|
|
|
1802
1803
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
1803
1804
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1804
1805
|
className: ModelSwitchSettings_module_css_default.hint,
|
|
1805
|
-
children: props.t("autoReviewTakeoverNote")
|
|
1806
|
+
children: props.t(props.capabilities.autoReview?.available ?? true ? "autoReviewTakeoverNote" : "autoReviewTakeoverWarning")
|
|
1806
1807
|
}),
|
|
1807
1808
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
1808
1809
|
className: ModelSwitchSettings_module_css_default.hint,
|
|
@@ -2289,6 +2290,7 @@ window.__ModuleLoader__.load({
|
|
|
2289
2290
|
autoReviewFollow: "跟随会话模型",
|
|
2290
2291
|
autoReviewHelp: "为官方 auto-review(Auto 权限预设的逐调用审查)指定审查模型;需启用官方 auto-review 能力包。关闭时审查跟随当前会话模型。本插件启用时,官方「自动审查」组件由本插件接管。",
|
|
2291
2292
|
autoReviewRouteHelp: "审查请求固定使用这里的 Provider/模型,与会话模型无关;保存后对下一次审查立即生效。",
|
|
2293
|
+
autoReviewTakeoverWarning: "审查路由当前未接管(官方「自动审查」行仍占着唯一注册位):工具调用会照常执行,但这些调用未经审查。重启 DSH 即可收敛;若重启后仍显示此提示,请把日志里的 fiber 链反馈给上游。",
|
|
2292
2294
|
autoReviewTakeoverNote: "官方「自动审查」组件已由本插件接管(提供可配置审查路由);在插件管理里把它重新打开会被再次关闭,并在日志里留下一条记录。",
|
|
2293
2295
|
compaction: "压缩模型",
|
|
2294
2296
|
compactionFollow: "跟随会话模型",
|
|
@@ -2348,6 +2350,7 @@ window.__ModuleLoader__.load({
|
|
|
2348
2350
|
autoReviewFollow: "Follows the session model",
|
|
2349
2351
|
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.",
|
|
2350
2352
|
autoReviewRouteHelp: "Review requests use exactly this provider/model regardless of the session model; saved changes apply to the very next review.",
|
|
2353
|
+
autoReviewTakeoverWarning: "The reviewer route is not held right now (the official Auto-review row still owns the single registration): tool calls keep running, but they are not reviewed. Restart DSH to converge; if this stays after a restart, report the fiber chain from the log upstream.",
|
|
2351
2354
|
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.",
|
|
2352
2355
|
compaction: "Compaction model",
|
|
2353
2356
|
compactionFollow: "Follows the conversation model",
|
package/lib/compaction.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as sessionProviderOf, r as resolveCapabilityRoute } from "./session-route-
|
|
1
|
+
import { i as sessionProviderOf, r as resolveCapabilityRoute } from "./session-route-D1zoZgf3.js";
|
|
2
2
|
//#region lib/types/compaction.js
|
|
3
3
|
/** Official compaction row this module supersedes. */
|
|
4
4
|
const OFFICIAL_COMPACTION_ROW = "compaction-basic";
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { o as normalizeProviderGroups } from "./session-route-D1zoZgf3.js";
|
|
2
|
+
import { i as autoReviewStatus } from "./auto-review-BLtwr83V.js";
|
|
2
3
|
import { ModelSwitchAdapterRegistry } from "./adapter-registry.js";
|
|
3
4
|
import { createRequire } from "node:module";
|
|
4
5
|
import z from "@deepseek-ai/schemastery";
|
|
@@ -103,7 +104,8 @@ const RUNTIME_CAPABILITIES = Object.freeze({
|
|
|
103
104
|
imageProviderAdapters: Object.freeze({
|
|
104
105
|
available: true,
|
|
105
106
|
providers: Object.freeze(["codex", "grok"])
|
|
106
|
-
})
|
|
107
|
+
}),
|
|
108
|
+
autoReview: Object.freeze({ available: true })
|
|
107
109
|
});
|
|
108
110
|
//#endregion
|
|
109
111
|
//#region lib/types/search-provider.js
|
|
@@ -771,6 +773,10 @@ var ModelSwitchRuntime = class extends Service {
|
|
|
771
773
|
available: true,
|
|
772
774
|
providers: catalog.map((provider) => provider.id),
|
|
773
775
|
catalog
|
|
776
|
+
},
|
|
777
|
+
autoReview: autoReviewStatus.held ? { available: true } : {
|
|
778
|
+
available: false,
|
|
779
|
+
reason: "auto-review-takeover"
|
|
774
780
|
}
|
|
775
781
|
};
|
|
776
782
|
}
|