dsh-model-switch 0.4.37 → 0.4.39
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 +35 -0
- package/lib/{auto-review-CvmQaRVf.js → auto-review-C61WZIYI.js} +365 -10
- package/lib/auto-review.js +1 -1
- package/lib/client.js +769 -12
- package/lib/index.js +86 -3
- package/lib/types/auto-review.d.ts +39 -6
- package/lib/types/capabilities-rpc.d.ts +10 -1
- package/lib/types/client/ModelSwitchSettings.d.ts +11 -1
- package/lib/types/client/locales.d.ts +18 -0
- package/lib/types/client/picker/PlanReviewCard.d.ts +8 -0
- package/lib/types/client-contract.d.ts +54 -0
- package/lib/types/host-settings.d.ts +17 -0
- package/lib/types/picker/discuss-plan.d.ts +43 -0
- package/lib/types/tool-allowlist.d.ts +113 -0
- package/lib/types/tool-catalog.d.ts +37 -0
- package/package.json +1 -1
- package/lib/auto-review-9SreBh-0.js +0 -855
- package/lib/auto-review-BLtwr83V.js +0 -738
- package/lib/auto-review-BoF2_YtP.js +0 -745
- package/lib/auto-review-CMHNfRuV.js +0 -1015
- package/lib/auto-review-D-0A5Amx.js +0 -859
- package/lib/auto-review-oan7iXY7.js +0 -987
|
@@ -1,738 +0,0 @@
|
|
|
1
|
-
import { i as sessionProviderOf, r as resolveCapabilityRoute } from "./session-route-D1zoZgf3.js";
|
|
2
|
-
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
-
/**
|
|
4
|
-
* The official package identity — the only handle on the row this plugin
|
|
5
|
-
* trusts. Matching by id alone could reach a group or another plugin's row, and
|
|
6
|
-
* a matched row is disabled (its fiber disposed).
|
|
7
|
-
*/
|
|
8
|
-
const OFFICIAL_AUTO_REVIEW_PACKAGE = "@deepseek-ai/dsh-experimental-auto-review";
|
|
9
|
-
/**
|
|
10
|
-
* Delays for the late re-claims after the attempt loop gave up (ms).
|
|
11
|
-
*
|
|
12
|
-
* `Entry.update({ disabled: true })` starts the displaced fiber's disposal but
|
|
13
|
-
* the loader does not await it, and a disposal that tears down a mounted gate,
|
|
14
|
-
* restores permission presets and unwinds fibers can take longer than the
|
|
15
|
-
* attempt loop's budget. These re-claims keep trying for a few more seconds so a
|
|
16
|
-
* hot-swapped install converges instead of needing a restart.
|
|
17
|
-
*/
|
|
18
|
-
const TAKEOVER_LATE_RETRY_DELAYS_MS = [
|
|
19
|
-
500,
|
|
20
|
-
1e3,
|
|
21
|
-
1500
|
|
22
|
-
];
|
|
23
|
-
/** Whether one loader entry is the official Auto-review row (strict package identity). */
|
|
24
|
-
function isOfficialAutoReviewRow(entry) {
|
|
25
|
-
if (entry === void 0) return false;
|
|
26
|
-
const pkg = entry.options?.name;
|
|
27
|
-
return typeof pkg === "string" && pkg === "@deepseek-ai/dsh-experimental-auto-review";
|
|
28
|
-
}
|
|
29
|
-
/** The first composed official Auto-review row, if the profile declares one. */
|
|
30
|
-
function officialAutoReviewEntry(loader) {
|
|
31
|
-
if (loader === void 0 || typeof loader.entries !== "function") return void 0;
|
|
32
|
-
for (const entry of loader.entries()) if (isOfficialAutoReviewRow(entry)) return entry;
|
|
33
|
-
}
|
|
34
|
-
/** The id one entry is known by (the loader's leaf id, not a nested path). */
|
|
35
|
-
function leafId(entry) {
|
|
36
|
-
const raw = entry.options?.id;
|
|
37
|
-
return typeof raw === "string" ? raw : void 0;
|
|
38
|
-
}
|
|
39
|
-
/** A loader entry the loader has not created a fiber for yet. */
|
|
40
|
-
function alreadyDisabled(entry) {
|
|
41
|
-
return entry.disabled === true;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Displace the official Auto-review row and take the registration back.
|
|
45
|
-
*
|
|
46
|
-
* The official row is disabled through the public `Entry.update` (which starts
|
|
47
|
-
* its fiber's disposal — the loader does not await it, so the claim retries
|
|
48
|
-
* until the displaced registration actually released). The Auto selection of
|
|
49
|
-
* live sessions is preserved around the displacement: the official gate's
|
|
50
|
-
* disposal moves `auto` sessions to `danger-full-access` by design, and this
|
|
51
|
-
* row's registration puts them back.
|
|
52
|
-
*
|
|
53
|
-
* @param ctx - this row's context, used only for logging.
|
|
54
|
-
* @param entry - the official row to displace.
|
|
55
|
-
* @param seam - the Auto-registration seam to yield and reclaim.
|
|
56
|
-
* @returns a promise settling once the takeover attempt loop finished.
|
|
57
|
-
*/
|
|
58
|
-
async function reclaimOfficialAutoReviewRow(ctx, entry, seam) {
|
|
59
|
-
const label = String(leafId(entry) ?? "auto-review");
|
|
60
|
-
/** One disable-then-claim step; true once this row holds the registration. */
|
|
61
|
-
const attempt = async () => {
|
|
62
|
-
try {
|
|
63
|
-
await entry.update({ disabled: true });
|
|
64
|
-
ctx.logger.info(`model-switch: disabled the official auto-review row ${label} (${OFFICIAL_AUTO_REVIEW_PACKAGE}); Model Switch's auto-review row owns the reviewer seam (configurable reviewer route)`);
|
|
65
|
-
} catch (cause) {
|
|
66
|
-
ctx.logger.info("model-switch: the displaced auto-review row could not be persisted as disabled; the takeover repeats next startup");
|
|
67
|
-
ctx.logger.info(String(cause));
|
|
68
|
-
}
|
|
69
|
-
return await seam.claim();
|
|
70
|
-
};
|
|
71
|
-
await seam.preserveAuto(async () => {
|
|
72
|
-
for (let index = 0; index < 60; index += 1) {
|
|
73
|
-
await new Promise((resolve) => setTimeout(resolve, index === 0 ? 0 : 50));
|
|
74
|
-
if (await attempt()) {
|
|
75
|
-
ctx.logger.info(`model-switch: displaced the official auto-review row ${label}; Auto review runs under the configurable reviewer route`);
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
ctx.logger.error(`model-switch: the official auto-review row ${label} kept the Auto registration; the reviewer route override is unavailable this session`);
|
|
80
|
-
});
|
|
81
|
-
scheduleLateReclaims(ctx, label, seam);
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Keep re-claiming for a few seconds after the attempt loop gave up.
|
|
85
|
-
*
|
|
86
|
-
* The displaced fiber's disposal starts on `Entry.update` but is not awaited by
|
|
87
|
-
* the loader, so it can outlast the attempt loop — which is exactly the hot-swap
|
|
88
|
-
* window where the official (unshielded) gate keeps serving. These bounded
|
|
89
|
-
* re-claims converge that window instead of leaving it to a restart. Timers are
|
|
90
|
-
* cleared when this row unloads.
|
|
91
|
-
*
|
|
92
|
-
* @param ctx - this row's context, used for logging and timer disposal.
|
|
93
|
-
* @param label - the official row's id, for diagnostics.
|
|
94
|
-
* @param seam - the Auto-registration seam to reclaim.
|
|
95
|
-
*/
|
|
96
|
-
function scheduleLateReclaims(ctx, label, seam) {
|
|
97
|
-
let stopped = false;
|
|
98
|
-
const timers = [];
|
|
99
|
-
const stop = () => {
|
|
100
|
-
stopped = true;
|
|
101
|
-
for (const timer of timers.splice(0)) clearTimeout(timer);
|
|
102
|
-
};
|
|
103
|
-
ctx.effect(function* () {
|
|
104
|
-
yield stop;
|
|
105
|
-
}, "model-switch: auto-review late takeover retries");
|
|
106
|
-
const run = (index) => {
|
|
107
|
-
if (stopped || index >= TAKEOVER_LATE_RETRY_DELAYS_MS.length) return;
|
|
108
|
-
const timer = setTimeout(() => {
|
|
109
|
-
(async () => {
|
|
110
|
-
if (stopped) return;
|
|
111
|
-
if (await seam.claim()) {
|
|
112
|
-
ctx.logger.info(`model-switch: reclaimed the Auto registration late from the official auto-review row ${label}; Auto review runs under the configurable reviewer route`);
|
|
113
|
-
stop();
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
run(index + 1);
|
|
117
|
-
})();
|
|
118
|
-
}, TAKEOVER_LATE_RETRY_DELAYS_MS[index]);
|
|
119
|
-
timers.push(timer);
|
|
120
|
-
};
|
|
121
|
-
run(0);
|
|
122
|
-
}
|
|
123
|
-
/**
|
|
124
|
-
* Install the takeover choreography for an official row that activates later.
|
|
125
|
-
*
|
|
126
|
-
* The plugin manager can re-enable the official row at any time (it writes
|
|
127
|
-
* `disabled: false` into the profile patch layer, which applies after every
|
|
128
|
-
* bundle layer). The `loader/patch-context` listener yields this row's
|
|
129
|
-
* registration **synchronously** before the entry's plugin `apply` runs, so the
|
|
130
|
-
* official row applies cleanly instead of colliding; the reclaim then disables
|
|
131
|
-
* it and takes the registration back.
|
|
132
|
-
*
|
|
133
|
-
* @param ctx - this row's context.
|
|
134
|
-
* @param seam - the Auto-registration seam to yield and reclaim.
|
|
135
|
-
* @returns the guarded reclaim function, so the caller's startup takeover and
|
|
136
|
-
* the listeners share one in-flight guard per entry.
|
|
137
|
-
*/
|
|
138
|
-
function installAutoReviewTakeover(ctx, seam) {
|
|
139
|
-
/** Entries already handled, so a repeated patch-context does not loop. */
|
|
140
|
-
const handled = /* @__PURE__ */ new Set();
|
|
141
|
-
/** Reclaims in flight, shared so two triggers for one entry join one cycle. */
|
|
142
|
-
const reclaiming = /* @__PURE__ */ new Map();
|
|
143
|
-
/** Disable the official row and take the registration back, once per entry. */
|
|
144
|
-
const reclaim = (entry) => {
|
|
145
|
-
const running = reclaiming.get(entry);
|
|
146
|
-
if (running !== void 0) return running;
|
|
147
|
-
const run = reclaimOfficialAutoReviewRow(ctx, entry, seam).finally(() => {
|
|
148
|
-
reclaiming.delete(entry);
|
|
149
|
-
});
|
|
150
|
-
reclaiming.set(entry, run);
|
|
151
|
-
return run;
|
|
152
|
-
};
|
|
153
|
-
/**
|
|
154
|
-
* The row an event names.
|
|
155
|
-
*
|
|
156
|
-
* The loader hands the emitting entry as the event's `this`, but cordis does
|
|
157
|
-
* not rebind `this` for listeners added through `ctx.on`, so a listener can
|
|
158
|
-
* also receive the payload as its first argument. Both shapes are accepted;
|
|
159
|
-
* the real Host always provides one of them.
|
|
160
|
-
*/
|
|
161
|
-
const entryOfEvent = (self, first) => {
|
|
162
|
-
const candidate = self ?? first;
|
|
163
|
-
return candidate !== null && typeof candidate === "object" && "options" in candidate ? candidate : void 0;
|
|
164
|
-
};
|
|
165
|
-
const listeners = [];
|
|
166
|
-
const listen = (event, listener) => {
|
|
167
|
-
const dispose = ctx.on?.(event, listener);
|
|
168
|
-
if (dispose !== void 0) listeners.push(dispose);
|
|
169
|
-
};
|
|
170
|
-
listen("loader/patch-context", function(first, second) {
|
|
171
|
-
const nextFn = typeof first === "function" ? first : second;
|
|
172
|
-
const entry = entryOfEvent(this, typeof first === "function" ? void 0 : first);
|
|
173
|
-
if (entry === void 0 || typeof nextFn !== "function" || !isOfficialAutoReviewRow(entry) || handled.has(entry)) return nextFn === void 0 ? void 0 : nextFn();
|
|
174
|
-
handled.add(entry);
|
|
175
|
-
seam.yieldRegistration();
|
|
176
|
-
reclaim(entry);
|
|
177
|
-
return nextFn();
|
|
178
|
-
});
|
|
179
|
-
listen("loader/partial-dispose", function(first) {
|
|
180
|
-
const entry = entryOfEvent(this, first);
|
|
181
|
-
if (entry === void 0 || !isOfficialAutoReviewRow(entry)) return;
|
|
182
|
-
handled.delete(entry);
|
|
183
|
-
seam.claim();
|
|
184
|
-
});
|
|
185
|
-
const loader = ctx.get("loader", false);
|
|
186
|
-
if (loader !== void 0 && typeof loader.entries === "function") for (const candidate of loader.entries()) {
|
|
187
|
-
if (!isOfficialAutoReviewRow(candidate)) continue;
|
|
188
|
-
if (alreadyDisabled(candidate)) continue;
|
|
189
|
-
reclaim(candidate);
|
|
190
|
-
}
|
|
191
|
-
const disposable = () => {
|
|
192
|
-
for (const dispose of listeners.splice(0)) dispose();
|
|
193
|
-
};
|
|
194
|
-
const effect = ctx.effect;
|
|
195
|
-
if (effect !== void 0) {
|
|
196
|
-
const registration = effect(function* () {
|
|
197
|
-
yield disposable;
|
|
198
|
-
}, "model-switch: auto-review takeover");
|
|
199
|
-
if (typeof registration === "function") listeners.push(registration);
|
|
200
|
-
}
|
|
201
|
-
return reclaim;
|
|
202
|
-
}
|
|
203
|
-
//#endregion
|
|
204
|
-
//#region lib/types/auto-review.js
|
|
205
|
-
/**
|
|
206
|
-
* Model Switch-owned Auto reviewer row.
|
|
207
|
-
*
|
|
208
|
-
* The official `@deepseek-ai/dsh-experimental-auto-review` gate hardwires its
|
|
209
|
-
* reviewer to the session's own request route (`header.config`). This row runs
|
|
210
|
-
* the official `apply` under a scoped child context whose `llm` service is
|
|
211
|
-
* shadowed by a wrapper that rewrites only `stream()`'s provider/model — and
|
|
212
|
-
* only when the reviewing session's derived route (its provider group's
|
|
213
|
-
* reviewer pair, else the deployment-wide reviewer card) pins one. With no
|
|
214
|
-
* route pinned the wrapper passes options through by identity, so the reviewer
|
|
215
|
-
* follows the session model exactly.
|
|
216
|
-
*
|
|
217
|
-
* Although the official gate omits `sessionId` and `purpose` from its frozen
|
|
218
|
-
* `GenerateOptions`, its `tools/pre-execute` waterfall listener receives
|
|
219
|
-
* `exec.agent` (`exec.agent.session`). After running `official.apply(scope)`,
|
|
220
|
-
* this module registers a `{ prepend: true }` `tools/pre-execute` listener on
|
|
221
|
-
* the same scope — which Cordis places ahead of the official gate's listener in
|
|
222
|
-
* the waterfall — and binds `exec.agent?.session` in an `AsyncLocalStorage`
|
|
223
|
-
* scope around `next()`. Every `scope.llm.stream()` call from `classifyRisk`
|
|
224
|
-
* therefore resolves the exact session being reviewed, with zero cross-session
|
|
225
|
-
* interference when multiple sessions review tool calls concurrently.
|
|
226
|
-
*
|
|
227
|
-
* The official gate stays an opt-in capability: the row mounts it only when
|
|
228
|
-
* the profile actually started with the experimental-auto-review bundle. Two
|
|
229
|
-
* rows cannot share the seam — `permissionPresets.registerAuto` is a single
|
|
230
|
-
* registration and the official probe is unshielded — so this row also
|
|
231
|
-
* **takes the seam over** (see `./auto-review-takeover.ts`): a composed or
|
|
232
|
-
* re-enabled official row is displaced through the released loader seams, the
|
|
233
|
-
* registration is handed over and reclaimed around that displacement, and the
|
|
234
|
-
* gate this row mounts keeps the defensive `permissionPresets` shadow so a
|
|
235
|
-
* degraded preset service can never fail a tool call again.
|
|
236
|
-
*
|
|
237
|
-
* @module dsh-model-switch/auto-review
|
|
238
|
-
*/
|
|
239
|
-
/** Cordis plugin name used by loader diagnostics. */
|
|
240
|
-
const name = "model-switch-auto-review";
|
|
241
|
-
/**
|
|
242
|
-
* Only the services this row's own code touches.
|
|
243
|
-
*
|
|
244
|
-
* The official gate used to run inside this row's fiber, so this list had to
|
|
245
|
-
* mirror every service the official `apply` reads. DSH 0.2.1 added
|
|
246
|
-
* `workingDirectory` to the official gate and the mirror went stale, which made
|
|
247
|
-
* every tool call fail with `cannot get property "workingDirectory" without
|
|
248
|
-
* inject`. The gate is now mounted as its own plugin (see {@link autoReviewGate}),
|
|
249
|
-
* so cordis resolves the official dependency list — including future additions —
|
|
250
|
-
* and this row declares only what it reads itself (`loader` drives the
|
|
251
|
-
* takeover).
|
|
252
|
-
*/
|
|
253
|
-
const inject = [
|
|
254
|
-
"llm",
|
|
255
|
-
"modelSwitch",
|
|
256
|
-
"loader"
|
|
257
|
-
];
|
|
258
|
-
/** Optional bundle whose composition opts a profile into Auto review. */
|
|
259
|
-
const AUTO_REVIEW_BUNDLE = "@deepseek-ai/dsh-experimental-auto-review";
|
|
260
|
-
/** The Auto preset name; `registerAuto` is what publishes it. */
|
|
261
|
-
const AUTO_PRESET = "auto";
|
|
262
|
-
/**
|
|
263
|
-
* Live Auto-reviewer takeover status, read by the runtime-capability snapshot.
|
|
264
|
-
*
|
|
265
|
-
* `held: false` means the official row kept the single `registerAuto` slot (or
|
|
266
|
-
* has not released it yet): the reviewer route is not configurable and the
|
|
267
|
-
* tool-call safety net allows unreviewed calls instead of failing them.
|
|
268
|
-
* `allowedCalls` counts the calls the net let through in that state, so the
|
|
269
|
-
* settings card can tell the human how much review was skipped.
|
|
270
|
-
*/
|
|
271
|
-
const autoReviewStatus = {
|
|
272
|
-
held: false,
|
|
273
|
-
allowedCalls: 0
|
|
274
|
-
};
|
|
275
|
-
/**
|
|
276
|
-
* Resolve the pinned reviewer route from live settings.
|
|
277
|
-
* @param settings - Model Switch settings carrying the optional reviewer pair.
|
|
278
|
-
* @returns Both identifiers trimmed and non-empty, or undefined (follow session).
|
|
279
|
-
*/
|
|
280
|
-
function autoReviewRouteOf(settings) {
|
|
281
|
-
const provider = settings?.autoReviewProvider?.trim();
|
|
282
|
-
const model = settings?.autoReviewModel?.trim();
|
|
283
|
-
if (provider === void 0 || provider === "" || model === void 0 || model === "") return void 0;
|
|
284
|
-
return {
|
|
285
|
-
provider,
|
|
286
|
-
model
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
/**
|
|
290
|
-
* Wrap the real llm service so only `stream()` route fields are rewritten.
|
|
291
|
-
*
|
|
292
|
-
* Non-`stream` members pass through bound to the real service (Proxy reads
|
|
293
|
-
* would otherwise hand methods the proxy as `this`). The route is re-read per
|
|
294
|
-
* call so a settings change applies to the very next review without a reload.
|
|
295
|
-
*
|
|
296
|
-
* @param real - the host's `ctx.llm`.
|
|
297
|
-
* @param route - live route resolver; undefined means "do not rewrite".
|
|
298
|
-
* @returns A structurally compatible llm face for the scoped context.
|
|
299
|
-
*/
|
|
300
|
-
function reviewLlm(real, route) {
|
|
301
|
-
return new Proxy(real, { get(target, property, _receiver) {
|
|
302
|
-
if (property === "stream") return (options) => {
|
|
303
|
-
const pinned = route();
|
|
304
|
-
return pinned === void 0 ? target.stream(options) : target.stream({
|
|
305
|
-
...options,
|
|
306
|
-
provider: pinned.provider,
|
|
307
|
-
model: pinned.model
|
|
308
|
-
});
|
|
309
|
-
};
|
|
310
|
-
const value = Reflect.get(target, property, target);
|
|
311
|
-
return typeof value === "function" ? value.bind(target) : value;
|
|
312
|
-
} });
|
|
313
|
-
}
|
|
314
|
-
/**
|
|
315
|
-
* Resolve the reviewer route for one session (or deployment-wide when no session is bound).
|
|
316
|
-
*
|
|
317
|
-
* @param ctx - host context whose modelSwitch settings and sessionProjections derive the route.
|
|
318
|
-
* @param session - the session whose tool call is being reviewed, when known.
|
|
319
|
-
* @returns The derived reviewer route, or undefined to follow the session model.
|
|
320
|
-
*/
|
|
321
|
-
function resolveSessionAutoReviewRoute(ctx, session) {
|
|
322
|
-
let settings;
|
|
323
|
-
try {
|
|
324
|
-
settings = ctx.modelSwitch.currentSettings();
|
|
325
|
-
} catch {
|
|
326
|
-
settings = void 0;
|
|
327
|
-
}
|
|
328
|
-
const provider = sessionProviderOf(ctx, session);
|
|
329
|
-
return resolveCapabilityRoute(settings, provider, "autoReview");
|
|
330
|
-
}
|
|
331
|
-
/**
|
|
332
|
-
* Install the outer `tools/pre-execute` correlation listener ahead of the official gate.
|
|
333
|
-
*
|
|
334
|
-
* Because Cordis `on(..., { prepend: true })` unshifts onto the event hook list,
|
|
335
|
-
* registering this listener AFTER `official.apply(scope)` places it at index 0
|
|
336
|
-
* — directly ahead of the official gate's own prepended listener — so
|
|
337
|
-
* `activeSession.run(exec.agent?.session, next)` wraps the entire official
|
|
338
|
-
* review execution.
|
|
339
|
-
*
|
|
340
|
-
* @param scope - the review scope context where the gate is mounted.
|
|
341
|
-
* @param activeSession - AsyncLocalStorage holding the session under review.
|
|
342
|
-
*/
|
|
343
|
-
function bindAutoReviewSessionCorrelation(scope, activeSession) {
|
|
344
|
-
scope.effect(function* () {
|
|
345
|
-
yield scope.on("tools/pre-execute", (exec, next) => activeSession.run(exec.agent?.session, next), { prepend: true });
|
|
346
|
-
}, "model-switch: auto-review session correlation");
|
|
347
|
-
}
|
|
348
|
-
/**
|
|
349
|
-
* Signatures of a broken reviewer seam, never of a real denial.
|
|
350
|
-
*
|
|
351
|
-
* The official gate's `tools/pre-execute` probe resolves its own
|
|
352
|
-
* `sessionProjections` injection before anything can catch it; when that fiber
|
|
353
|
-
* chain breaks it throws `cannot get required service ... in inactive context`
|
|
354
|
-
* (or, on older lines, `cannot get property ... without inject`) straight into
|
|
355
|
-
* the tool call. `TRANSPORT` covers a reviewer that cannot reach its route at
|
|
356
|
-
* all. A denial is a *return* (`{ kind: 'deny' }`), so it never matches.
|
|
357
|
-
*/
|
|
358
|
-
const REVIEWER_INFRA_FAILURE_SIGNATURES = [
|
|
359
|
-
"inactive context",
|
|
360
|
-
"cannot get required service",
|
|
361
|
-
"cannot get property",
|
|
362
|
-
"without inject",
|
|
363
|
-
"TRANSPORT"
|
|
364
|
-
];
|
|
365
|
-
/**
|
|
366
|
-
* Whether a reviewer-path throw is a broken seam rather than a real decision.
|
|
367
|
-
* @param cause - whatever the waterfall threw.
|
|
368
|
-
* @returns true when the reviewer itself could not run.
|
|
369
|
-
*/
|
|
370
|
-
function isReviewerInfrastructureFailure(cause) {
|
|
371
|
-
const text = cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause);
|
|
372
|
-
return REVIEWER_INFRA_FAILURE_SIGNATURES.some((signature) => text.includes(signature));
|
|
373
|
-
}
|
|
374
|
-
/**
|
|
375
|
-
* Keep a broken reviewer seam from failing tool calls.
|
|
376
|
-
*
|
|
377
|
-
* Registered on this row's own context through `{ prepend: true }`, so it sits
|
|
378
|
-
* ahead of the official gate's listener and stays live even when the gate could
|
|
379
|
-
* not be mounted (the takeover lost `registerAuto`, or the displaced row has
|
|
380
|
-
* not released it yet). That is the one window the defensive
|
|
381
|
-
* `permissionPresets` shadow cannot cover: the shadow lives in this row's review
|
|
382
|
-
* scope, while the serving gate resolves the real service from the profile.
|
|
383
|
-
*
|
|
384
|
-
* The waterfall only decides whether the body runs; the body dispatches after
|
|
385
|
-
* it returns, so allowing here never masks a tool failure. Reviewer transport
|
|
386
|
-
* and quota errors keep failing closed — only seam breakage is allowed through.
|
|
387
|
-
*
|
|
388
|
-
* @param ctx - this row's context (the listener lives and dies with the plugin).
|
|
389
|
-
* @param options - diagnostics plus the degraded-state predicate.
|
|
390
|
-
*/
|
|
391
|
-
function installToolCallSafetyNet(ctx, options = {}) {
|
|
392
|
-
let reported = false;
|
|
393
|
-
const handler = async (_exec, next) => {
|
|
394
|
-
try {
|
|
395
|
-
return await next();
|
|
396
|
-
} catch (cause) {
|
|
397
|
-
if (!isReviewerInfrastructureFailure(cause)) throw cause;
|
|
398
|
-
if (options.isDegraded !== void 0 && !options.isDegraded()) throw cause;
|
|
399
|
-
if (!reported) {
|
|
400
|
-
reported = true;
|
|
401
|
-
const detail = options.fiberChain === void 0 ? "" : `; fiber chain: ${options.fiberChain()}`;
|
|
402
|
-
ctx.logger.warn(`model-switch: the Auto reviewer path is broken (${String(cause)}); tool calls run unreviewed instead of failing${detail}`);
|
|
403
|
-
}
|
|
404
|
-
autoReviewStatus.allowedCalls += 1;
|
|
405
|
-
return { kind: "allow" };
|
|
406
|
-
}
|
|
407
|
-
};
|
|
408
|
-
ctx.effect(function* () {
|
|
409
|
-
yield ctx.on("tools/pre-execute", handler, { prepend: true });
|
|
410
|
-
}, "model-switch: reviewer-path safety net");
|
|
411
|
-
}
|
|
412
|
-
/** The preset name the service reports when no configured preset matches. */
|
|
413
|
-
const CUSTOM_PRESET = "custom";
|
|
414
|
-
/** Read the live permission-preset service without requiring an injection. */
|
|
415
|
-
function permissionPresetsOf(ctx) {
|
|
416
|
-
try {
|
|
417
|
-
const face = ctx.get("permissionPresets", false);
|
|
418
|
-
return face !== null && typeof face === "object" ? face : void 0;
|
|
419
|
-
} catch {
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
/**
|
|
424
|
-
* A compact dump of the fiber chain a service resolution walks.
|
|
425
|
-
*
|
|
426
|
-
* `cannot get required service "sessionProjections" in inactive context` is
|
|
427
|
-
* thrown by the first fiber in the caller's chain that declares the service
|
|
428
|
-
* without a resolved snapshot; the chain names that fiber directly.
|
|
429
|
-
* @param ctx - the context whose chain is walked.
|
|
430
|
-
* @returns One `uid/state/name/inject` segment per fiber, outermost last.
|
|
431
|
-
*/
|
|
432
|
-
function fiberChainOf(ctx) {
|
|
433
|
-
const parts = [];
|
|
434
|
-
let node = ctx;
|
|
435
|
-
for (let step = 0; step < 8; step += 1) {
|
|
436
|
-
const fiber = node?.fiber;
|
|
437
|
-
if (fiber === void 0) break;
|
|
438
|
-
const injects = Object.keys(fiber.inject ?? {}).join("|");
|
|
439
|
-
parts.push(`uid=${String(fiber.uid)} state=${String(fiber.state)} name=${String(fiber.name)} inject=[${injects}]`);
|
|
440
|
-
node = fiber.parent;
|
|
441
|
-
}
|
|
442
|
-
return parts.length === 0 ? "no fiber chain" : parts.join(" -> ");
|
|
443
|
-
}
|
|
444
|
-
/**
|
|
445
|
-
* Wrap the permission-preset service so the official gate's per-call probe can
|
|
446
|
-
* never fail a tool call, and broker its single Auto registration.
|
|
447
|
-
*
|
|
448
|
-
* DSH's Auto gate starts every `tools/pre-execute` with
|
|
449
|
-
* `permissionPresets.current(session)`, and that method resolves the presets
|
|
450
|
-
* service's own `sessionProjections` injection. When the presets fiber has
|
|
451
|
-
* turned inactive the probe throws `cannot get required service
|
|
452
|
-
* "sessionProjections" in inactive context` outside the gate's reviewer error
|
|
453
|
-
* wrapper, so every tool call dies. This wrapper resolves the service lazily
|
|
454
|
-
* (so the shadow is in place even while the service is still activating),
|
|
455
|
-
* degrades the probe to `custom` (the gate then skips review and calls
|
|
456
|
-
* `next()`), and reports the lost capability once — with the fiber chain that
|
|
457
|
-
* failed resolution, so the next occurrence names the inactive fiber.
|
|
458
|
-
*
|
|
459
|
-
* `registerAuto` is **brokered**: the gate yields the disposer into its own
|
|
460
|
-
* effect, but the takeover must be able to hand the single registration to a
|
|
461
|
-
* displacing row and take it back without unmounting the gate (the gate's
|
|
462
|
-
* disposal would move live `auto` sessions to `danger-full-access`).
|
|
463
|
-
*
|
|
464
|
-
* @param resolve - resolves the host's permission-preset service per call.
|
|
465
|
-
* @param log - the row logger used for the single degradation report.
|
|
466
|
-
* @param chain - diagnostic dump of the resolution chain; defaults to empty.
|
|
467
|
-
* @returns A managed preset face.
|
|
468
|
-
*/
|
|
469
|
-
function defensivePermissionPresets(resolve, log, chain = () => "") {
|
|
470
|
-
let reported = false;
|
|
471
|
-
let admit;
|
|
472
|
-
let release;
|
|
473
|
-
const degrade = (operation, error) => {
|
|
474
|
-
if (reported) return;
|
|
475
|
-
reported = true;
|
|
476
|
-
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()}]`);
|
|
477
|
-
};
|
|
478
|
-
const requireService = () => {
|
|
479
|
-
const face = resolve();
|
|
480
|
-
if (face === void 0) throw new Error("permission: the permission-preset service is unavailable");
|
|
481
|
-
return face;
|
|
482
|
-
};
|
|
483
|
-
return {
|
|
484
|
-
current(session) {
|
|
485
|
-
try {
|
|
486
|
-
return requireService().current(session);
|
|
487
|
-
} catch (error) {
|
|
488
|
-
degrade("current()", error);
|
|
489
|
-
return CUSTOM_PRESET;
|
|
490
|
-
}
|
|
491
|
-
},
|
|
492
|
-
set(session, preset) {
|
|
493
|
-
try {
|
|
494
|
-
return requireService().set(session, preset);
|
|
495
|
-
} catch (error) {
|
|
496
|
-
degrade("set()", error);
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
},
|
|
500
|
-
registerAuto(next) {
|
|
501
|
-
admit = next;
|
|
502
|
-
release = requireService().registerAuto(next);
|
|
503
|
-
return () => {
|
|
504
|
-
admit = void 0;
|
|
505
|
-
const dispose = release;
|
|
506
|
-
release = void 0;
|
|
507
|
-
return dispose?.();
|
|
508
|
-
};
|
|
509
|
-
},
|
|
510
|
-
releaseForTakeover() {
|
|
511
|
-
const dispose = release;
|
|
512
|
-
release = void 0;
|
|
513
|
-
dispose?.();
|
|
514
|
-
},
|
|
515
|
-
reclaimRegistration() {
|
|
516
|
-
if (release !== void 0) return true;
|
|
517
|
-
if (admit === void 0) return false;
|
|
518
|
-
try {
|
|
519
|
-
release = requireService().registerAuto(admit);
|
|
520
|
-
return true;
|
|
521
|
-
} catch {
|
|
522
|
-
return false;
|
|
523
|
-
}
|
|
524
|
-
},
|
|
525
|
-
get registered() {
|
|
526
|
-
return release !== void 0;
|
|
527
|
-
}
|
|
528
|
-
};
|
|
529
|
-
}
|
|
530
|
-
/**
|
|
531
|
-
* Build the scoped context the official gate runs under.
|
|
532
|
-
* @param ctx - host context whose modelSwitch settings pin the reviewer route.
|
|
533
|
-
* @param activeSession - optional AsyncLocalStorage correlating each `tools/pre-execute` call to its session.
|
|
534
|
-
* @param presets - optional managed preset face shared with the row's takeover.
|
|
535
|
-
* @returns A child context shadowing the llm service and the permission-preset
|
|
536
|
-
* service (always: a gate that sees the raw service can fail a tool call).
|
|
537
|
-
*/
|
|
538
|
-
function reviewScope(ctx, activeSession = new AsyncLocalStorage(), presets = defensivePermissionPresets(() => permissionPresetsOf(ctx), ctx.logger, () => fiberChainOf(ctx))) {
|
|
539
|
-
return ctx.extend({
|
|
540
|
-
llm: reviewLlm(ctx.llm, () => resolveSessionAutoReviewRoute(ctx, activeSession.getStore())),
|
|
541
|
-
permissionPresets: presets
|
|
542
|
-
});
|
|
543
|
-
}
|
|
544
|
-
/** Structural read of launcher-provided profile facts (the app-boot seam). */
|
|
545
|
-
function autoReviewBundleActive(ctx) {
|
|
546
|
-
let profile;
|
|
547
|
-
try {
|
|
548
|
-
profile = ctx.get("profileContext", false);
|
|
549
|
-
} catch {
|
|
550
|
-
return false;
|
|
551
|
-
}
|
|
552
|
-
if (profile === null || typeof profile !== "object") return false;
|
|
553
|
-
const bundles = profile.startedBundles;
|
|
554
|
-
return Array.isArray(bundles) && bundles.includes(AUTO_REVIEW_BUNDLE);
|
|
555
|
-
}
|
|
556
|
-
/** Structural read of the loader service the takeover drives. */
|
|
557
|
-
function loaderOf(ctx) {
|
|
558
|
-
try {
|
|
559
|
-
const loader = ctx.get("loader", false);
|
|
560
|
-
return loader !== null && typeof loader === "object" ? loader : void 0;
|
|
561
|
-
} catch {
|
|
562
|
-
return;
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
/**
|
|
566
|
-
* Wrap the official gate as a plugin that declares its own dependencies.
|
|
567
|
-
*
|
|
568
|
-
* Directly calling `official.apply(scope)` ran the gate's code on this row's
|
|
569
|
-
* fiber, so every service the gate reads had to be mirrored in this row's
|
|
570
|
-
* `inject`. Mounting it instead lets cordis resolve the official list, which is
|
|
571
|
-
* what keeps a released Host's new dependency (0.2.1's `workingDirectory`) from
|
|
572
|
-
* breaking every tool call. The hooks let the caller observe whether the gate
|
|
573
|
-
* actually ran before it binds the session-correlation listener.
|
|
574
|
-
*
|
|
575
|
-
* @param gate - the official module (or an equivalent structural gate).
|
|
576
|
-
* @param hooks - applied/failure observers; the failure is rethrown.
|
|
577
|
-
* @returns A cordis plugin mounting the official gate unchanged.
|
|
578
|
-
*/
|
|
579
|
-
function autoReviewGate(gate, hooks = {}) {
|
|
580
|
-
return {
|
|
581
|
-
name: gate.name ?? "experimental-auto-review",
|
|
582
|
-
...gate.inject === void 0 ? {} : { inject: [...gate.inject] },
|
|
583
|
-
apply(gateCtx) {
|
|
584
|
-
try {
|
|
585
|
-
gate.apply(gateCtx);
|
|
586
|
-
} catch (error) {
|
|
587
|
-
hooks.onFailure?.(error);
|
|
588
|
-
throw error;
|
|
589
|
-
}
|
|
590
|
-
hooks.onApplied?.();
|
|
591
|
-
}
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
/**
|
|
595
|
-
* The gate fiber mounted by the current row instance.
|
|
596
|
-
*
|
|
597
|
-
* `registerAuto` is a single live registration owned by the gate's effect
|
|
598
|
-
* lifetime, and its disposer is what clears the Auto preset. A re-applied row
|
|
599
|
-
* must await the previous gate's unloading before the next registration runs,
|
|
600
|
-
* otherwise the new `registerAuto` throws `already registered` and its effect
|
|
601
|
-
* cleanup leaves the catalog without Auto.
|
|
602
|
-
*/
|
|
603
|
-
let lastGateFiber;
|
|
604
|
-
/**
|
|
605
|
-
* Mount the official gate under the reviewer-route scope and own the seam.
|
|
606
|
-
*
|
|
607
|
-
* A profile that never enabled the optional bundle keeps no Auto preset and no
|
|
608
|
-
* reviewer; a deployment missing the official package reports the lost
|
|
609
|
-
* capability explicitly instead of guessing another path. A composed (or
|
|
610
|
-
* plugin-manager re-enabled) official row is displaced first — see
|
|
611
|
-
* `./auto-review-takeover.ts` for the choreography and why the yield is
|
|
612
|
-
* synchronous.
|
|
613
|
-
*/
|
|
614
|
-
async function apply(ctx) {
|
|
615
|
-
if (!autoReviewBundleActive(ctx)) {
|
|
616
|
-
ctx.logger.info("model-switch: auto-review bundle is not active in this profile; the reviewer route override stays unmounted");
|
|
617
|
-
return;
|
|
618
|
-
}
|
|
619
|
-
let official;
|
|
620
|
-
try {
|
|
621
|
-
official = await import("@deepseek-ai/dsh-experimental-auto-review");
|
|
622
|
-
} catch (cause) {
|
|
623
|
-
ctx.logger.error("model-switch: @deepseek-ai/dsh-experimental-auto-review is unavailable; the Auto reviewer cannot run at all");
|
|
624
|
-
ctx.logger.error(cause);
|
|
625
|
-
return;
|
|
626
|
-
}
|
|
627
|
-
const presets = defensivePermissionPresets(() => permissionPresetsOf(ctx), ctx.logger, () => fiberChainOf(ctx));
|
|
628
|
-
const activeSession = new AsyncLocalStorage();
|
|
629
|
-
const scope = reviewScope(ctx, activeSession, presets);
|
|
630
|
-
installToolCallSafetyNet(ctx, {
|
|
631
|
-
fiberChain: () => fiberChainOf(ctx),
|
|
632
|
-
isDegraded: () => !presets.registered
|
|
633
|
-
});
|
|
634
|
-
let gateReady = false;
|
|
635
|
-
let mountInFlight;
|
|
636
|
-
/** Sessions currently on the Auto preset (best-effort). */
|
|
637
|
-
const rememberAuto = () => {
|
|
638
|
-
const remembered = [];
|
|
639
|
-
try {
|
|
640
|
-
const sessions = ctx.get("sessions", false);
|
|
641
|
-
for (const session of sessions?.list?.() ?? []) if (presets.current(session) === AUTO_PRESET) remembered.push(session);
|
|
642
|
-
} catch {}
|
|
643
|
-
return remembered;
|
|
644
|
-
};
|
|
645
|
-
const restoreAuto = (remembered) => {
|
|
646
|
-
for (const session of remembered) try {
|
|
647
|
-
presets.set(session, AUTO_PRESET);
|
|
648
|
-
} catch {}
|
|
649
|
-
};
|
|
650
|
-
/**
|
|
651
|
-
* Run one displacement step without losing the sessions' Auto selection.
|
|
652
|
-
*
|
|
653
|
-
* Disposing a gate moves live `auto` sessions to `danger-full-access` by
|
|
654
|
-
* design (the official teardown runs on both the displaced row and this
|
|
655
|
-
* row's own re-mounts); the remembered selection is restored afterwards.
|
|
656
|
-
*/
|
|
657
|
-
const preserveAuto = async (action) => {
|
|
658
|
-
const remembered = rememberAuto();
|
|
659
|
-
await action();
|
|
660
|
-
restoreAuto(remembered);
|
|
661
|
-
};
|
|
662
|
-
/** Mount this row's shielded gate exactly once; failures dispose and report. */
|
|
663
|
-
const mountGate = () => {
|
|
664
|
-
mountInFlight ??= (async () => {
|
|
665
|
-
let outcome = "failed";
|
|
666
|
-
await preserveAuto(async () => {
|
|
667
|
-
const previous = lastGateFiber;
|
|
668
|
-
lastGateFiber = void 0;
|
|
669
|
-
gateReady = false;
|
|
670
|
-
if (previous !== void 0) try {
|
|
671
|
-
await previous.dispose();
|
|
672
|
-
} catch (cause) {
|
|
673
|
-
ctx.logger.info("model-switch: the previous auto-review gate had already unloaded; continuing with the new one");
|
|
674
|
-
ctx.logger.info(String(cause));
|
|
675
|
-
}
|
|
676
|
-
let failure;
|
|
677
|
-
let applied = false;
|
|
678
|
-
const fiber = scope.plugin(autoReviewGate(official, {
|
|
679
|
-
onApplied: () => {
|
|
680
|
-
applied = true;
|
|
681
|
-
bindAutoReviewSessionCorrelation(scope, activeSession);
|
|
682
|
-
},
|
|
683
|
-
onFailure: (error) => {
|
|
684
|
-
failure = error;
|
|
685
|
-
}
|
|
686
|
-
}));
|
|
687
|
-
try {
|
|
688
|
-
await fiber;
|
|
689
|
-
} catch (cause) {
|
|
690
|
-
failure ??= cause;
|
|
691
|
-
}
|
|
692
|
-
if (failure !== void 0) {
|
|
693
|
-
try {
|
|
694
|
-
await fiber.dispose();
|
|
695
|
-
} catch {}
|
|
696
|
-
ctx.logger.warn(`model-switch: the Auto reviewer gate did not mount (${String(failure)}); the displaced registration may still be releasing`);
|
|
697
|
-
outcome = "failed";
|
|
698
|
-
return;
|
|
699
|
-
}
|
|
700
|
-
if (!applied) {
|
|
701
|
-
ctx.logger.info("model-switch: the official auto-review gate is waiting for its host services; the reviewer route override is not bound yet");
|
|
702
|
-
outcome = "waiting";
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
lastGateFiber = fiber;
|
|
706
|
-
gateReady = true;
|
|
707
|
-
outcome = "applied";
|
|
708
|
-
});
|
|
709
|
-
return outcome;
|
|
710
|
-
})().finally(() => {
|
|
711
|
-
mountInFlight = void 0;
|
|
712
|
-
});
|
|
713
|
-
return mountInFlight;
|
|
714
|
-
};
|
|
715
|
-
/** Make this row's Auto registration live; true once it is held. */
|
|
716
|
-
const claim = async () => {
|
|
717
|
-
let held;
|
|
718
|
-
if (presets.registered) held = true;
|
|
719
|
-
else if (gateReady) held = presets.reclaimRegistration();
|
|
720
|
-
else held = await mountGate() !== "failed";
|
|
721
|
-
autoReviewStatus.held = held;
|
|
722
|
-
return held;
|
|
723
|
-
};
|
|
724
|
-
const reclaim = installAutoReviewTakeover(ctx, {
|
|
725
|
-
yieldRegistration: () => presets.releaseForTakeover(),
|
|
726
|
-
claim,
|
|
727
|
-
preserveAuto
|
|
728
|
-
});
|
|
729
|
-
const entry = officialAutoReviewEntry(loaderOf(ctx));
|
|
730
|
-
if (entry !== void 0 && entry.disabled !== true) await reclaim(entry);
|
|
731
|
-
else await claim();
|
|
732
|
-
if (!presets.registered) {
|
|
733
|
-
autoReviewStatus.held = false;
|
|
734
|
-
ctx.logger.error("model-switch: the Auto registration is not held by this row, so Auto review runs without the configurable reviewer route; the reviewer path is shielded instead of failing tool calls. Restart DSH to converge the reviewer row — a hot install cannot always displace the official row in time");
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
//#endregion
|
|
738
|
-
export { bindAutoReviewSessionCorrelation as a, installToolCallSafetyNet as c, resolveSessionAutoReviewRoute as d, reviewLlm as f, autoReviewStatus as i, isReviewerInfrastructureFailure as l, autoReviewGate as n, defensivePermissionPresets as o, reviewScope as p, autoReviewRouteOf as r, inject as s, apply as t, name as u };
|