dsh-advisor 0.4.0 → 0.5.0
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/README.i18n.yaml +2 -2
- package/README.md +31 -22
- package/README.zh.md +31 -22
- package/lib/advisor-runtime.js +1 -1
- package/lib/client/advisor-card.d.ts +18 -14
- package/lib/client/advisor-session.d.ts +75 -0
- package/lib/client/advisor-store.d.ts +131 -0
- package/lib/client/index.d.ts +18 -13
- package/lib/client/locales.d.ts +15 -0
- package/lib/client.js +375 -14
- package/lib/commands.d.ts +220 -27
- package/lib/commands.js +260 -34
- package/lib/commands.js.map +1 -1
- package/lib/config.d.ts +74 -20
- package/lib/config.js +74 -20
- package/lib/config.js.map +1 -1
- package/lib/delivery.d.ts +9 -9
- package/lib/delivery.js +10 -12
- package/lib/delivery.js.map +1 -1
- package/lib/gateway.d.ts +139 -24
- package/lib/gateway.js +138 -27
- package/lib/gateway.js.map +1 -1
- package/lib/index.d.ts +5 -5
- package/lib/index.js +483 -82
- package/lib/index.js.map +1 -1
- package/lib/kinds.d.ts +79 -51
- package/lib/kinds.js +60 -43
- package/lib/kinds.js.map +1 -1
- package/lib/settings.d.ts +56 -86
- package/lib/settings.js +48 -91
- package/lib/settings.js.map +1 -1
- package/lib/transcript.d.ts +11 -9
- package/lib/transcript.js +25 -16
- package/lib/transcript.js.map +1 -1
- package/lib/tui-settings.d.ts +10 -10
- package/lib/tui-settings.js +10 -10
- package/lib/tui-settings.js.map +1 -1
- package/lib/tui.js +39 -5
- package/lib/tui.js.map +1 -1
- package/package.json +24 -23
package/lib/index.js
CHANGED
|
@@ -31,11 +31,11 @@
|
|
|
31
31
|
* (`ctx.get('tuiSettingsSections') !== undefined`), so the hint names the TUI
|
|
32
32
|
* `/settings` Advisor section as a write path exactly while the seam is
|
|
33
33
|
* mounted; the readback itself stays session-less and read-only.
|
|
34
|
-
* Settings (plan dsh-advisor-settings-n2): the plugin-row config
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* derived state (immuneTurns /
|
|
38
|
-
* without a restart.
|
|
34
|
+
* Settings (plan dsh-advisor-settings-n2): the plugin-row entry config's six
|
|
35
|
+
* live fields are schema-volatile (`src/config.ts`), read live through the
|
|
36
|
+
* bridge source (`src/settings.ts`); committed volatile edits (Loader
|
|
37
|
+
* `loader/volatile-update`) re-apply derived state (immuneTurns /
|
|
38
|
+
* maxDeltaMessages / per-session runtimes) without a restart.
|
|
39
39
|
* Config gateway (plan dsh-advisor-settings-gateway-n5): `apply` also
|
|
40
40
|
* registers the host-side `AdvisorConfigGateway` (`src/gateway.ts`) — the
|
|
41
41
|
* `/api/advisor/get` + `/api/advisor/set` endpoints (explicit
|
|
@@ -45,14 +45,67 @@
|
|
|
45
45
|
*
|
|
46
46
|
* @module dsh-advisor
|
|
47
47
|
*/
|
|
48
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
49
|
+
if (value !== null && value !== void 0) {
|
|
50
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
51
|
+
var dispose, inner;
|
|
52
|
+
if (async) {
|
|
53
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
54
|
+
dispose = value[Symbol.asyncDispose];
|
|
55
|
+
}
|
|
56
|
+
if (dispose === void 0) {
|
|
57
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
58
|
+
dispose = value[Symbol.dispose];
|
|
59
|
+
if (async) inner = dispose;
|
|
60
|
+
}
|
|
61
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
62
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
63
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
64
|
+
}
|
|
65
|
+
else if (async) {
|
|
66
|
+
env.stack.push({ async: true });
|
|
67
|
+
}
|
|
68
|
+
return value;
|
|
69
|
+
};
|
|
70
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
71
|
+
return function (env) {
|
|
72
|
+
function fail(e) {
|
|
73
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
74
|
+
env.hasError = true;
|
|
75
|
+
}
|
|
76
|
+
var r, s = 0;
|
|
77
|
+
function next() {
|
|
78
|
+
while (r = env.stack.pop()) {
|
|
79
|
+
try {
|
|
80
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
81
|
+
if (r.dispose) {
|
|
82
|
+
var result = r.dispose.call(r.value);
|
|
83
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
84
|
+
}
|
|
85
|
+
else s |= 1;
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
fail(e);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
92
|
+
if (env.hasError) throw env.error;
|
|
93
|
+
}
|
|
94
|
+
return next();
|
|
95
|
+
};
|
|
96
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
97
|
+
var e = new Error(message);
|
|
98
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
99
|
+
});
|
|
48
100
|
import { resolveAdvisorConfig } from './config.js';
|
|
49
101
|
import { installAdvisorSettings } from './settings.js';
|
|
50
|
-
import { AdvisorConfigGateway, advisorTypertContribution } from './gateway.js';
|
|
102
|
+
import { AdvisorConfigGateway, advisorSessionError, advisorTypertContribution } from './gateway.js';
|
|
51
103
|
import { SessionTranscriptObserver } from './transcript.js';
|
|
52
104
|
import { AdvisorRuntime } from './advisor-runtime.js';
|
|
53
105
|
import { AdvisorDelivery } from './delivery.js';
|
|
54
106
|
import { DEFAULT_ADVISOR_SYSTEM_PROMPT } from './prompts.js';
|
|
55
|
-
import {
|
|
107
|
+
import { deadline, timeoutOf } from '@deepseek-ai/dsh-timeout';
|
|
108
|
+
import { AdvisorSessionOverrides, registerAdvisorCommands, summarizeSystemPrompt, ADVISOR_MODEL_VALIDATION_TIMEOUT_MS } from './commands.js';
|
|
56
109
|
import { installTuiClient } from './tui.js';
|
|
57
110
|
import { TUI_SETTINGS_SECTIONS, installTuiSettingsSection } from './tui-settings.js';
|
|
58
111
|
export const name = 'dsh-advisor';
|
|
@@ -64,15 +117,13 @@ export { Config } from './config.js';
|
|
|
64
117
|
* fibers (observed: 3 active); with the global session/event subscription every
|
|
65
118
|
* instance would observe every session and N×-review/N×-call per round. The
|
|
66
119
|
* FIRST apply to claim the reviewer role wires the observer/runtime/delivery
|
|
67
|
-
* and the /advisor commands; later instances
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* can take over. The flag rides globalThis so it survives even module-copy
|
|
75
|
-
* divergence. */
|
|
120
|
+
* and the /advisor commands; later instances keep only their settings bridge
|
|
121
|
+
* (each fiber's bridge listens on its own fiber's `loader/volatile-update` —
|
|
122
|
+
* the owning-filter keeps them independent) and stay inert otherwise. The
|
|
123
|
+
* claim is taken only AFTER the construction-time config gate (qc1 W-4: a
|
|
124
|
+
* rejected first row never leaves the flag claimed) and is released when the
|
|
125
|
+
* claiming fiber is disposed, so a later re-apply/re-mount can take over. The
|
|
126
|
+
* flag rides globalThis so it survives even module-copy divergence. */
|
|
76
127
|
const REVIEWER_KEY = '__dshAdvisorReviewer__';
|
|
77
128
|
function claimReviewer() {
|
|
78
129
|
const g = globalThis;
|
|
@@ -81,16 +132,72 @@ function claimReviewer() {
|
|
|
81
132
|
g[REVIEWER_KEY] = true;
|
|
82
133
|
return true;
|
|
83
134
|
}
|
|
135
|
+
/** Capability-owned code stamped onto the model-validation deadline's TimeoutReason. */
|
|
136
|
+
const ADVISOR_MODEL_VALIDATION_TIMEOUT = 'ADVISOR_MODEL_VALIDATION_TIMEOUT';
|
|
137
|
+
/**
|
|
138
|
+
* Fuse several upstream signals into one (spec §5.3 validation): the fused
|
|
139
|
+
* signal aborts when ANY upstream aborts. Used to combine the invoking
|
|
140
|
+
* command's cancellation signal with the owner-teardown signal — a hung
|
|
141
|
+
* `resolveModelInfo` lookup that honors cancellation aborts with whichever
|
|
142
|
+
* lands first, and the `dsh-timeout` deadline adds the 60 s bound on top.
|
|
143
|
+
* `cleanup` removes the listeners (the teardown signal outlives every
|
|
144
|
+
* short-lived validation).
|
|
145
|
+
*/
|
|
146
|
+
function fuseSignals(...upstreams) {
|
|
147
|
+
const controller = new AbortController();
|
|
148
|
+
const onAbort = (event) => {
|
|
149
|
+
controller.abort(event.target.reason);
|
|
150
|
+
};
|
|
151
|
+
for (const upstream of upstreams) {
|
|
152
|
+
if (upstream === undefined)
|
|
153
|
+
continue;
|
|
154
|
+
if (upstream.aborted)
|
|
155
|
+
controller.abort(upstream.reason);
|
|
156
|
+
else
|
|
157
|
+
upstream.addEventListener('abort', onAbort);
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
signal: controller.signal,
|
|
161
|
+
cleanup: () => {
|
|
162
|
+
for (const upstream of upstreams)
|
|
163
|
+
upstream?.removeEventListener('abort', onAbort);
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Race one promise against a signal (same shape as the runtime's per-chunk
|
|
169
|
+
* `raceIteratorNext`): settle with the promise's result, or `'aborted'` when
|
|
170
|
+
* the signal aborts first. Needed because `LlmRuntime.resolveModelInfo` only
|
|
171
|
+
* THREADS the signal into the adapter — an adapter whose lookup ignores
|
|
172
|
+
* cancellation would otherwise wedge the validation past the deadline; the
|
|
173
|
+
* race makes the 60 s bound (and the owner-teardown/command aborts) real for
|
|
174
|
+
* the caller regardless of adapter behavior.
|
|
175
|
+
*/
|
|
176
|
+
function raceSignal(promise, signal) {
|
|
177
|
+
if (signal.aborted)
|
|
178
|
+
return Promise.resolve('aborted');
|
|
179
|
+
return new Promise((resolve, reject) => {
|
|
180
|
+
const onAbort = () => resolve('aborted');
|
|
181
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
182
|
+
promise.then((value) => {
|
|
183
|
+
signal.removeEventListener('abort', onAbort);
|
|
184
|
+
resolve(value);
|
|
185
|
+
}, (error) => {
|
|
186
|
+
signal.removeEventListener('abort', onAbort);
|
|
187
|
+
reject(error);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
}
|
|
84
191
|
export function apply(ctx, config) {
|
|
85
192
|
// T2: the explicit provider/model gate — no model call without both
|
|
86
193
|
// (spec §5.2). Unknown keys / malformed config throw here, rejecting the
|
|
87
194
|
// plugin row at load; the gate resolves to disabled-with-reason instead.
|
|
88
195
|
//
|
|
89
|
-
// T1-settings (plan dsh-advisor-settings-n2): the plugin-row config
|
|
90
|
-
//
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
196
|
+
// T1-settings (plan dsh-advisor-settings-n2): the plugin-row entry config's
|
|
197
|
+
// six live fields are schema-volatile (0.1.7-rc.1). The runtime reads the
|
|
198
|
+
// LIVE values through the bridge source (per-field volatile reference
|
|
199
|
+
// unwrap); the Loader commits edits into those references without a remount
|
|
200
|
+
// and announces them via `loader/volatile-update`. The hard gate is applied
|
|
94
201
|
// to every read: `resolveAdvisorConfig` stays the SSOT for the
|
|
95
202
|
// disabled-with-reason resolution.
|
|
96
203
|
const bridge = installAdvisorSettings(ctx, config);
|
|
@@ -112,8 +219,16 @@ export function apply(ctx, config) {
|
|
|
112
219
|
// registrations are fiber effects (unregistered when this fiber disposes),
|
|
113
220
|
// so a re-apply/re-mount can take over. The instance needs no handle here:
|
|
114
221
|
// the typertGateway dispatches through `ctx.get('advisor')`.
|
|
222
|
+
//
|
|
223
|
+
// B2 (issue #88 web session control): the session endpoints route through
|
|
224
|
+
// the SAME controller the `/advisor model` commands drive — the face below
|
|
225
|
+
// is assigned by the elected (reviewer-claiming) fiber AFTER its controller
|
|
226
|
+
// exists; the gateway resolves it lazily per request, so a gateway on a
|
|
227
|
+
// fiber that lost the service-key race (or that never claimed the reviewer
|
|
228
|
+
// role) answers `advisor/unavailable` in data and stays inert.
|
|
229
|
+
let sessionControl;
|
|
115
230
|
try {
|
|
116
|
-
new AdvisorConfigGateway(ctx, bridge);
|
|
231
|
+
new AdvisorConfigGateway(ctx, bridge, () => sessionControl);
|
|
117
232
|
}
|
|
118
233
|
catch (error) {
|
|
119
234
|
if (!(error instanceof Error) || !error.message.includes('has been registered'))
|
|
@@ -139,18 +254,18 @@ export function apply(ctx, config) {
|
|
|
139
254
|
});
|
|
140
255
|
const sourceConfig = () => bridge.source();
|
|
141
256
|
const resolved = () => resolveAdvisorConfig(sourceConfig());
|
|
142
|
-
// qc2 W-1 containment:
|
|
143
|
-
// unknown key
|
|
144
|
-
// live hot path — every read that can run inside an event
|
|
145
|
-
// through the safe wrappers, which catch resolver throws and
|
|
146
|
-
// disabled-with-reason carrying the message, so gate semantics hold
|
|
147
|
-
// model call can start) and handlers stay functional. The LOAD-TIME
|
|
257
|
+
// qc2 W-1 containment: an entry config the resolver rejects (e.g. an
|
|
258
|
+
// unknown key that survived the non-strict schemastery object merge) must
|
|
259
|
+
// never wedge the live hot path — every read that can run inside an event
|
|
260
|
+
// handler goes through the safe wrappers, which catch resolver throws and
|
|
261
|
+
// return disabled-with-reason carrying the message, so gate semantics hold
|
|
262
|
+
// (no model call can start) and handlers stay functional. The LOAD-TIME
|
|
148
263
|
// plugin-row throw contract is unchanged: construction-time reads below
|
|
149
264
|
// (delivery/observer latches) still use the throwing `resolved()`, so a bad
|
|
150
265
|
// entry rejects the plugin row at load (config.test.ts ⑤).
|
|
151
266
|
const safeFallback = (reason) => {
|
|
152
267
|
// S1 (gateway readConfig parity): when the raw source is still readable,
|
|
153
|
-
// seed the scalar latches from it — an invalid
|
|
268
|
+
// seed the scalar latches from it — an invalid config only drops the
|
|
154
269
|
// offending keys, so /advisor config (and /advisor status) never
|
|
155
270
|
// misreport immuneTurns / maxDeltaMessages / systemPrompt vs the web
|
|
156
271
|
// card's /api/advisor/get readback.
|
|
@@ -177,9 +292,23 @@ export function apply(ctx, config) {
|
|
|
177
292
|
return safeFallback(error instanceof Error ? error.message : String(error));
|
|
178
293
|
}
|
|
179
294
|
};
|
|
295
|
+
// Spec §5.3 — the ONE effective resolver: (1) the global shape is validated
|
|
296
|
+
// first (a malformed entry still throws → safeFallback: fail closed for
|
|
297
|
+
// every session); (2) enable = session enable override ?? global; (3) route
|
|
298
|
+
// = the session's COMPLETE modelOverride pair ?? the composed global pair —
|
|
299
|
+
// the pair is atomic, so the levels are never merged; (4) the explicit pair
|
|
300
|
+
// gate (§5.2) applies AFTER this resolution on the effective values: a
|
|
301
|
+
// complete session pair satisfies a pairless global default, while an
|
|
302
|
+
// invalid global schema can never be bypassed.
|
|
180
303
|
const safeEffective = (sessionId) => {
|
|
181
304
|
try {
|
|
182
|
-
|
|
305
|
+
const source = sourceConfig();
|
|
306
|
+
const pair = overrides.model(sessionId);
|
|
307
|
+
return resolveAdvisorConfig({
|
|
308
|
+
...source,
|
|
309
|
+
enabled: effectiveEnabled(sessionId),
|
|
310
|
+
...(pair === undefined ? {} : { provider: pair.provider, model: pair.model }),
|
|
311
|
+
});
|
|
183
312
|
}
|
|
184
313
|
catch (error) {
|
|
185
314
|
return safeFallback(error instanceof Error ? error.message : String(error));
|
|
@@ -194,17 +323,30 @@ export function apply(ctx, config) {
|
|
|
194
323
|
// commands start/stop per-session runtimes WITHOUT touching the persisted
|
|
195
324
|
// config (spec §4 mapping — omp `/advisor` semantics). Ephemeral: entries
|
|
196
325
|
// are cleared on `agent/disposed` / `session/disposed` below. Seeded with
|
|
197
|
-
// the
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
|
|
326
|
+
// the LIVE config switch read through the bridge (the raw `config` fields
|
|
327
|
+
// are volatile references on a Loader composition — an unwrapped snapshot
|
|
328
|
+
// shares the source with `safeResolved`): a config-enabled-but-gate-blocked
|
|
329
|
+
// session (enabled without provider/model) then re-derives the
|
|
330
|
+
// disabled-with-reason through the resolver, so `/advisor status` shows the
|
|
331
|
+
// reason (spec §5.2; qc3 I-1) — the gate itself still blocks every runtime
|
|
332
|
+
// (the resolver is the SSOT for the gate).
|
|
333
|
+
const overrides = new AdvisorSessionOverrides(bridge.source().enabled);
|
|
203
334
|
const effectiveEnabled = (sessionId) => overrides.effective(sessionId);
|
|
204
335
|
// Live-path alias: every consumer reads the effective config through the
|
|
205
336
|
// safe wrapper (qc2 W-1 — a throwing resolver must not break the
|
|
206
337
|
// session/event handler or `/advisor status`).
|
|
207
338
|
const effectiveConfig = (sessionId) => safeEffective(sessionId);
|
|
339
|
+
// n4 root-cause (host-observed NO_ADAPTER): this plugin's ctx may live in an
|
|
340
|
+
// isolated scope whose local llm service lacks the provider adapters (adapter
|
|
341
|
+
// registrations live on the application root's LlmRuntime). The APPLICATION
|
|
342
|
+
// ROOT's llm serves BOTH the advisor model calls (ensureRuntime) and the
|
|
343
|
+
// `/advisor model set` validation lookups (spec §5.3).
|
|
344
|
+
const appRootLlm = () => ctx.root?.get?.('llm') ?? ctx.llm;
|
|
345
|
+
// Spec §5.3 — model-validation teardown signal: fused into every in-flight
|
|
346
|
+
// `/advisor model set` lookup, so owner teardown aborts them promptly; the
|
|
347
|
+
// generation wipe (disposeAll, below) makes any completion that still
|
|
348
|
+
// settles unable to commit.
|
|
349
|
+
const validationTeardown = new AbortController();
|
|
208
350
|
// T3+T4: per-session transcript observation wired into one advisor runtime
|
|
209
351
|
// per session. On each stepped reviewable turn/end a bounded markdown delta
|
|
210
352
|
// is rendered and queued on the session's runtime; the runtime drains it
|
|
@@ -232,7 +374,9 @@ export function apply(ctx, config) {
|
|
|
232
374
|
* creation and compared on every settings change (qc3 W-1 / qc1 W-2): only
|
|
233
375
|
* a signature change tears the runtime down — an immuneTurns/
|
|
234
376
|
* maxDeltaMessages-only edit updates the latches in place and keeps every
|
|
235
|
-
* in-flight call and backlog.
|
|
377
|
+
* in-flight call and backlog. The triple reads the EFFECTIVE config
|
|
378
|
+
* (spec §5.3), so a session-pinned pair freezes the signature against later
|
|
379
|
+
* global pair edits — global edits reach inheritors, never pinned sessions.
|
|
236
380
|
*/
|
|
237
381
|
const runtimeSignatures = new Map();
|
|
238
382
|
const runtimeSignature = (sessionId) => {
|
|
@@ -267,12 +411,7 @@ export function apply(ctx, config) {
|
|
|
267
411
|
provider: effective.provider,
|
|
268
412
|
model: effective.model,
|
|
269
413
|
systemPrompt: safeResolved().systemPrompt || DEFAULT_ADVISOR_SYSTEM_PROMPT,
|
|
270
|
-
|
|
271
|
-
// an isolated scope whose local llm service lacks the provider adapters
|
|
272
|
-
// (adapter registrations live on the application root's LlmRuntime). Resolve
|
|
273
|
-
// the llm service from the APPLICATION ROOT so the advisor's model calls
|
|
274
|
-
// reach the registered deepseek-official adapter.
|
|
275
|
-
llm: ctx.root?.get?.('llm') ?? ctx.llm,
|
|
414
|
+
llm: appRootLlm(),
|
|
276
415
|
onNote: (note) => {
|
|
277
416
|
// Accepted notes only — the runtime's emission guard (T5) already
|
|
278
417
|
// filtered suppressed ones. T6 routes the accepted note to the primary
|
|
@@ -298,14 +437,36 @@ export function apply(ctx, config) {
|
|
|
298
437
|
runtimeSignatures.delete(sessionId);
|
|
299
438
|
runtime.dispose();
|
|
300
439
|
};
|
|
440
|
+
/**
|
|
441
|
+
* Apply a committed effective-route change (spec §5.3): abort the session's
|
|
442
|
+
* old advisor call + drop its backlog (disposeRuntime), re-seed the observer
|
|
443
|
+
* cursor to the current transcript length, and recreate the runtime — the
|
|
444
|
+
* change takes effect at the next new reviewable delta (no replay of the
|
|
445
|
+
* current window). A disabled session keeps its (new) pin for a later
|
|
446
|
+
* `/advisor on`; an unchanged effective route restarts nothing.
|
|
447
|
+
* @param before - the effective config captured BEFORE the override state
|
|
448
|
+
* changed (the caller mutates `overrides` between the capture and here).
|
|
449
|
+
*/
|
|
450
|
+
const applyRouteChange = (sessionId, before, sessionLength) => {
|
|
451
|
+
if (!effectiveEnabled(sessionId))
|
|
452
|
+
return;
|
|
453
|
+
const after = effectiveConfig(sessionId);
|
|
454
|
+
if (after.provider === before.provider && after.model === before.model)
|
|
455
|
+
return;
|
|
456
|
+
if (sessionLength !== undefined)
|
|
457
|
+
observer.seedTo(sessionId, sessionLength);
|
|
458
|
+
disposeRuntime(sessionId);
|
|
459
|
+
ensureRuntime(sessionId);
|
|
460
|
+
};
|
|
301
461
|
// n4 QC F-6: claim the single-reviewer role HERE — after every
|
|
302
462
|
// construction-time throwing read (the delivery latch above resolves the
|
|
303
463
|
// config and can throw on a rejected row), so a first fiber whose config
|
|
304
464
|
// fails the gate never leaves the flag claimed (qc1 W-4). Non-reviewer
|
|
305
465
|
// instances stop here — observer/runtime/delivery and the /advisor commands
|
|
306
|
-
// are wired only by the single claimed reviewer.
|
|
307
|
-
// (installAdvisorSettings above)
|
|
308
|
-
//
|
|
466
|
+
// are wired only by the single claimed reviewer. Their own settings bridge
|
|
467
|
+
// (installAdvisorSettings above) stays live: it is per-fiber by
|
|
468
|
+
// construction, nothing to dedupe (qc1 W-5's registration is gone with the
|
|
469
|
+
// 0.1.7-rc.1 namespace model).
|
|
309
470
|
const reviewer = claimReviewer();
|
|
310
471
|
if (!reviewer) {
|
|
311
472
|
ctx.logger('advisor').debug('non-reviewer instance — observer/runtime/commands skipped (single-reviewer guard)');
|
|
@@ -314,9 +475,14 @@ export function apply(ctx, config) {
|
|
|
314
475
|
// qc1 W-4: release the claim when THIS (reviewer) fiber is disposed, so a
|
|
315
476
|
// later re-apply/re-mount (plugin-row removal, composition reload, host hot
|
|
316
477
|
// reload) can take over instead of leaving the advisor silently inert for
|
|
317
|
-
// the process lifetime. Registered only on the claiming fiber.
|
|
478
|
+
// the process lifetime. Registered only on the claiming fiber. The teardown
|
|
479
|
+
// also owns the override state (spec §5.3): abort in-flight model
|
|
480
|
+
// validations and wipe every per-session override entry/generation — a
|
|
481
|
+
// delayed validation completion can never commit after the owner is gone.
|
|
318
482
|
ctx.effect(() => () => {
|
|
319
483
|
delete globalThis[REVIEWER_KEY];
|
|
484
|
+
validationTeardown.abort('advisor owner disposed');
|
|
485
|
+
overrides.disposeAll();
|
|
320
486
|
}, 'advisor: release reviewer claim');
|
|
321
487
|
const observer = new SessionTranscriptObserver({
|
|
322
488
|
maxDeltaMessages: resolved().maxDeltaMessages,
|
|
@@ -372,9 +538,26 @@ export function apply(ctx, config) {
|
|
|
372
538
|
// creates the runtime eagerly (plan T4) when the session is enabled and
|
|
373
539
|
// registers the agent in the KD-4 delivery map (T6); the observer fallback
|
|
374
540
|
// covers pre-existing agents (KD-4-style robustness).
|
|
541
|
+
//
|
|
542
|
+
// 0.1.6-alpha.2 seam: `agent/created` became a SERIAL event — its listeners
|
|
543
|
+
// run in order and are AWAITED before creation resolves, and a throw or
|
|
544
|
+
// rejection FAILS creation and skips later listeners (0.1.5-rc.2 dispatched
|
|
545
|
+
// it fire-and-forget). Two obligations follow for this handler:
|
|
546
|
+
// - return `undefined`: the listener contract is
|
|
547
|
+
// `undefined | Promise<undefined>`, so a bare `void` body no longer
|
|
548
|
+
// typechecks against the host Events table;
|
|
549
|
+
// - never throw: an advisory-only plugin must not be able to fail the
|
|
550
|
+
// primary agent's creation, so setup failures are contained and logged
|
|
551
|
+
// here rather than crossing the event boundary.
|
|
375
552
|
ctx.on('agent/created', ({ agent }) => {
|
|
376
|
-
|
|
377
|
-
|
|
553
|
+
try {
|
|
554
|
+
ensureRuntime(agent.id);
|
|
555
|
+
delivery.registerAgent(agent);
|
|
556
|
+
}
|
|
557
|
+
catch (error) {
|
|
558
|
+
ctx.logger('advisor').warn('advisor: agent/created setup failed — contained', { error, sessionId: agent.id });
|
|
559
|
+
}
|
|
560
|
+
return undefined;
|
|
378
561
|
}, { global: true });
|
|
379
562
|
ctx.on('agent/disposed', ({ agent }) => {
|
|
380
563
|
observer.disposeSession(agent.id);
|
|
@@ -391,17 +574,18 @@ export function apply(ctx, config) {
|
|
|
391
574
|
// T1-settings live re-apply: construction-time latches (immuneTurns on the
|
|
392
575
|
// delivery, maxDeltaMessages on the observer, systemPrompt + provider/model
|
|
393
576
|
// on each per-session runtime) are re-derived from the NEW source on every
|
|
394
|
-
// committed
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
//
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
577
|
+
// committed volatile edit — the Loader's `loader/volatile-update` event,
|
|
578
|
+
// dispatched to this fiber only after the committed values are readable —
|
|
579
|
+
// and re-applied: delivery/observer update in place, per-session runtimes
|
|
580
|
+
// rebuild only when their runtime-affecting signature actually changed
|
|
581
|
+
// (qc3 W-1 / qc1 W-2: an immuneTurns/maxDeltaMessages-only edit must not
|
|
582
|
+
// abort in-flight advisor calls or drop backlogs). The S4 gate is
|
|
583
|
+
// re-applied by the resolver on every read, so a config edit can never
|
|
584
|
+
// start a gated model call (SSOT unchanged); the config-level fallback
|
|
585
|
+
// switch follows the live source so new sessions pick up an enabled edit
|
|
586
|
+
// immediately. An entry config the resolver rejects (qc2 W-1 — unknown
|
|
587
|
+
// key) stops the advisor without wedging the re-apply path, and the
|
|
588
|
+
// last-good latches stay until the config is repaired.
|
|
405
589
|
bridge.onChange(() => {
|
|
406
590
|
let next;
|
|
407
591
|
try {
|
|
@@ -427,8 +611,16 @@ export function apply(ctx, config) {
|
|
|
427
611
|
delivery.setImmuneTurns(next.immuneTurns);
|
|
428
612
|
observer.setMaxDeltaMessages(next.maxDeltaMessages);
|
|
429
613
|
overrides.setConfigEnabled(sourceConfig().enabled);
|
|
614
|
+
// Candidates = live runtimes ∪ sessions holding an override. The second
|
|
615
|
+
// set covers an `/advisor on`-enabled session whose runtime is ABSENT
|
|
616
|
+
// because it was gate-blocked: when the defaults become usable (a global
|
|
617
|
+
// pair is committed) it must gain its runtime here, not wait for its next
|
|
618
|
+
// delta (plan: include enabled-gate-blocked sessions with no runtime when
|
|
619
|
+
// defaults become usable). The signature compare still gates actual
|
|
620
|
+
// rebuilds; ensureRuntime re-applies the post-gate gate on every call.
|
|
621
|
+
const candidates = new Set([...runtimes.keys(), ...overrides.overrideSessionIds()]);
|
|
430
622
|
let rebuilt = 0;
|
|
431
|
-
for (const sessionId of
|
|
623
|
+
for (const sessionId of candidates) {
|
|
432
624
|
if (runtimeSignatures.get(sessionId) === runtimeSignature(sessionId))
|
|
433
625
|
continue;
|
|
434
626
|
disposeRuntime(sessionId);
|
|
@@ -449,7 +641,34 @@ export function apply(ctx, config) {
|
|
|
449
641
|
// no full-history replay) and creates/resumes/recoveries the session runtime;
|
|
450
642
|
// `/advisor off` disposes it (abort in-flight, drop backlog). The S4 gate
|
|
451
643
|
// reason is re-derived through the config resolver, the SSOT for the
|
|
452
|
-
// disabled-with-reason text (spec §5.2).
|
|
644
|
+
// disabled-with-reason text (spec §5.2). `/advisor model set|reset` mutate
|
|
645
|
+
// the runtime-only session pair (spec §5.3) through the generation-fenced
|
|
646
|
+
// validation + applyRouteChange seams below.
|
|
647
|
+
const sessionStatus = (sessionId) => {
|
|
648
|
+
const runtime = runtimes.get(sessionId);
|
|
649
|
+
const effective = effectiveConfig(sessionId);
|
|
650
|
+
const pair = overrides.model(sessionId);
|
|
651
|
+
// Spec §5.3: the status reports the EFFECTIVE route, labeled by source.
|
|
652
|
+
// The label is `session` only when the effective pair IS the session's
|
|
653
|
+
// pin (a malformed global fails closed through safeFallback — provider/
|
|
654
|
+
// model undefined — and a pinned session under it reports no pair, with
|
|
655
|
+
// the reason carried by disabledReason).
|
|
656
|
+
const source = effective.provider === undefined || effective.model === undefined
|
|
657
|
+
? undefined
|
|
658
|
+
: pair !== undefined && pair.provider === effective.provider && pair.model === effective.model
|
|
659
|
+
? 'session'
|
|
660
|
+
: 'global';
|
|
661
|
+
return {
|
|
662
|
+
enabled: effective.enabled,
|
|
663
|
+
...(effective.disabledReason === undefined ? {} : { disabledReason: effective.disabledReason }),
|
|
664
|
+
provider: effective.provider,
|
|
665
|
+
model: effective.model,
|
|
666
|
+
...(source === undefined ? {} : { modelSource: source }),
|
|
667
|
+
runtimeStatus: runtime?.status() ?? 'disabled',
|
|
668
|
+
pendingCount: runtime?.pendingCount ?? 0,
|
|
669
|
+
lastActivityAt: runtime?.lastActivity,
|
|
670
|
+
};
|
|
671
|
+
};
|
|
453
672
|
const controller = {
|
|
454
673
|
setEnabled(sessionId, enabled, sessionLength) {
|
|
455
674
|
// Recovery, not just a switch flip: `/advisor on` (and toggle-to-on)
|
|
@@ -486,29 +705,121 @@ export function apply(ctx, config) {
|
|
|
486
705
|
}
|
|
487
706
|
},
|
|
488
707
|
getStatus(sessionId) {
|
|
489
|
-
|
|
708
|
+
return sessionStatus(sessionId);
|
|
709
|
+
},
|
|
710
|
+
// Spec §5.3: the effective route for `/advisor model` — the resolver SSOT
|
|
711
|
+
// (effectiveConfig) supplies the pair, the session pin supplies the label.
|
|
712
|
+
getModelRoute(sessionId) {
|
|
490
713
|
const effective = effectiveConfig(sessionId);
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
714
|
+
const pair = overrides.model(sessionId);
|
|
715
|
+
const source = pair !== undefined && pair.provider === effective.provider && pair.model === effective.model
|
|
716
|
+
? 'session'
|
|
717
|
+
: 'global';
|
|
718
|
+
return { provider: effective.provider, model: effective.model, source };
|
|
719
|
+
},
|
|
720
|
+
// Spec §5.3 — validate, then commit, an atomic session pair. Fence FIRST
|
|
721
|
+
// (bump the generation), so a newer set/reset — or any state wipe — makes
|
|
722
|
+
// this attempt stale. The lookup rides the app-root LLM service, bound to
|
|
723
|
+
// the 60 s deadline fused with the invoking command's signal and the owner
|
|
724
|
+
// teardown signal, with NO automatic retry; failure leaves the previous
|
|
725
|
+
// selection untouched. Catalog membership is advisory — a successful
|
|
726
|
+
// resolution is the acceptance gate. Selection never starts a generation
|
|
727
|
+
// call and never touches the enable override.
|
|
728
|
+
async setModel(sessionId, provider, model, agent, signal) {
|
|
729
|
+
const generation = overrides.beginModelGeneration(sessionId);
|
|
730
|
+
const fused = fuseSignals(signal, validationTeardown.signal);
|
|
731
|
+
try {
|
|
732
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
733
|
+
try {
|
|
734
|
+
const deadlineHandle = __addDisposableResource(env_1, deadline(fused.signal, ADVISOR_MODEL_VALIDATION_TIMEOUT_MS, ADVISOR_MODEL_VALIDATION_TIMEOUT)
|
|
735
|
+
// Race the lookup against the fused signal: a hung adapter lookup that
|
|
736
|
+
// ignores cancellation settles here as 'aborted' at the deadline, so
|
|
737
|
+
// the 60 s bound holds regardless of adapter behavior. There is NO
|
|
738
|
+
// automatic retry — one lookup, one outcome.
|
|
739
|
+
, false);
|
|
740
|
+
// Race the lookup against the fused signal: a hung adapter lookup that
|
|
741
|
+
// ignores cancellation settles here as 'aborted' at the deadline, so
|
|
742
|
+
// the 60 s bound holds regardless of adapter behavior. There is NO
|
|
743
|
+
// automatic retry — one lookup, one outcome.
|
|
744
|
+
const resolvedInfo = await raceSignal(appRootLlm().resolveModelInfo(provider, model, deadlineHandle.signal), deadlineHandle.signal);
|
|
745
|
+
if (resolvedInfo === 'aborted') {
|
|
746
|
+
// Deadline fired → the timed-out failure; otherwise the invoking
|
|
747
|
+
// command was cancelled or the owner/session went away.
|
|
748
|
+
if (timeoutOf(deadlineHandle.signal, ADVISOR_MODEL_VALIDATION_TIMEOUT) !== undefined) {
|
|
749
|
+
return {
|
|
750
|
+
kind: 'failed',
|
|
751
|
+
reason: `model validation timed out after ${ADVISOR_MODEL_VALIDATION_TIMEOUT_MS}ms`,
|
|
752
|
+
};
|
|
753
|
+
}
|
|
754
|
+
if (signal?.aborted === true)
|
|
755
|
+
return { kind: 'cancelled' };
|
|
756
|
+
return { kind: 'gone' };
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
catch (e_1) {
|
|
760
|
+
env_1.error = e_1;
|
|
761
|
+
env_1.hasError = true;
|
|
762
|
+
}
|
|
763
|
+
finally {
|
|
764
|
+
__disposeResources(env_1);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
catch (error) {
|
|
768
|
+
// A rejecting lookup (unknown route, adapter throw) — classify the
|
|
769
|
+
// same abort vocabulary, else report the failure verbatim.
|
|
770
|
+
if (signal?.aborted === true)
|
|
771
|
+
return { kind: 'cancelled' };
|
|
772
|
+
if (validationTeardown.signal.aborted)
|
|
773
|
+
return { kind: 'gone' };
|
|
774
|
+
return { kind: 'failed', reason: error instanceof Error ? error.message : String(error) };
|
|
775
|
+
}
|
|
776
|
+
finally {
|
|
777
|
+
fused.cleanup();
|
|
778
|
+
}
|
|
779
|
+
// Fence checks AFTER the lookup settled — session/owner liveness first
|
|
780
|
+
// (a dispose/reload cannot be undone by a delayed validation completion),
|
|
781
|
+
// then the generation: a newer set/reset supersedes unresolved older
|
|
782
|
+
// work while the session is still live.
|
|
783
|
+
if (validationTeardown.signal.aborted || ctx.agents.get(sessionId) === undefined) {
|
|
784
|
+
return { kind: 'gone' };
|
|
785
|
+
}
|
|
786
|
+
if (overrides.modelGeneration(sessionId) !== generation)
|
|
787
|
+
return { kind: 'superseded' };
|
|
788
|
+
// Commit: the route-change effect fires only when the effective pair
|
|
789
|
+
// actually changed (an equal-default pin is recorded, not restarted).
|
|
790
|
+
const before = effectiveConfig(sessionId);
|
|
791
|
+
overrides.setModel(sessionId, { provider, model });
|
|
792
|
+
applyRouteChange(sessionId, before, agent.session.seq);
|
|
793
|
+
const status = sessionStatus(sessionId);
|
|
794
|
+
return before.provider === status.provider && before.model === status.model
|
|
795
|
+
? { kind: 'unchanged', status }
|
|
796
|
+
: { kind: 'committed', status };
|
|
797
|
+
},
|
|
798
|
+
// Spec §5.3 — drop the pin and re-inherit the CURRENT global defaults.
|
|
799
|
+
// Synchronous (no lookup needed). The generation bumps FIRST — a reset is
|
|
800
|
+
// a newer model command even when there is no pin to remove (noop), so it
|
|
801
|
+
// supersedes any in-flight validation. Reset never touches the enable
|
|
802
|
+
// override, and reset to a missing global pair "succeeds" while the
|
|
803
|
+
// post-reset status reports the gate-blocked/no-call state.
|
|
804
|
+
resetModel(sessionId, sessionLength) {
|
|
805
|
+
overrides.beginModelGeneration(sessionId);
|
|
806
|
+
const before = effectiveConfig(sessionId);
|
|
807
|
+
if (!overrides.clearModel(sessionId))
|
|
808
|
+
return { kind: 'noop' };
|
|
809
|
+
applyRouteChange(sessionId, before, sessionLength);
|
|
810
|
+
return { kind: 'reset', status: sessionStatus(sessionId) };
|
|
500
811
|
},
|
|
501
812
|
// T2 (plan dsh-advisor-tui-client-n8): the composed-config readback.
|
|
502
|
-
// Session-less BY DESIGN — reads `safeResolved()` (
|
|
503
|
-
//
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
// `
|
|
507
|
-
//
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
//
|
|
813
|
+
// Session-less BY DESIGN — reads `safeResolved()` (the live entry config
|
|
814
|
+
// with the hard gate applied), the SAME bridge source the web card reads
|
|
815
|
+
// through `resolveAdvisorConfig` (`/api/advisor/get` has no session
|
|
816
|
+
// either). NEVER `effectiveConfig`/`safeEffective` here: those bake the
|
|
817
|
+
// per-session `/advisor` override into `enabled`, and a `/advisor off`
|
|
818
|
+
// session toggle must never make the config readback misreport the
|
|
819
|
+
// persisted config. Runtime state stays owned by `status`; this read
|
|
820
|
+
// reports config only. Every field comes from that one resolved value;
|
|
821
|
+
// the systemPrompt summary is the first line (≤ 80 chars) of
|
|
822
|
+
// `resolved.systemPrompt`, '' → unset.
|
|
512
823
|
getConfig() {
|
|
513
824
|
const resolved = safeResolved();
|
|
514
825
|
return {
|
|
@@ -541,6 +852,96 @@ export function apply(ctx, config) {
|
|
|
541
852
|
};
|
|
542
853
|
},
|
|
543
854
|
};
|
|
855
|
+
// B2 (issue #88 web session control): the elected owner's session face —
|
|
856
|
+
// the web surface of the SAME `controller` above (`/api/advisor/getSession`
|
|
857
|
+
// + `/api/advisor/setSessionModel`; selection null = reset). Snapshot reads
|
|
858
|
+
// ride `sessionStatus` (the resolver SSOT); writes delegate to
|
|
859
|
+
// `controller.setModel` / `controller.resetModel`, so pre-commit
|
|
860
|
+
// `resolveModelInfo` validation, per-session generation fencing, and the
|
|
861
|
+
// route-change semantics are inherited from the B1 command face, never
|
|
862
|
+
// reimplemented. Liveness is checked BEFORE any controller call (a dead
|
|
863
|
+
// target must not allocate a generation — `setModel` bumps one on entry),
|
|
864
|
+
// using the SAME seam the controller's own post-lookup fence uses
|
|
865
|
+
// (`ctx.agents.get`), so a disposed/unknown session is rejected without
|
|
866
|
+
// allocating state. SessionId is NOT authorization: the request still
|
|
867
|
+
// crossed the Connection Host/Origin + browser-auth boundary upstream;
|
|
868
|
+
// this face adds no ACL layer. Business outcomes ride the returned data
|
|
869
|
+
// (plugin-domain error tags) — the dsh failure vocabulary stays frozen.
|
|
870
|
+
const sessionLiveError = (sessionId) => ctx.agents.get(sessionId) === undefined
|
|
871
|
+
? advisorSessionError('advisor/session-unknown', `advisor: session ${sessionId} is not live`)
|
|
872
|
+
: undefined;
|
|
873
|
+
const snapshotOfStatus = (sessionId, status) => {
|
|
874
|
+
const effective = status.provider !== undefined && status.model !== undefined
|
|
875
|
+
? { provider: status.provider, model: status.model }
|
|
876
|
+
: undefined;
|
|
877
|
+
// The pin, when present, always IS the effective route (atomic override),
|
|
878
|
+
// so `modelSource === 'session'` marks a pinned `modelOverride`. A pin
|
|
879
|
+
// equal to the global default still reports `session` — the explicit pin
|
|
880
|
+
// is protected from later default edits.
|
|
881
|
+
const pinned = status.modelSource === 'session' ? effective : undefined;
|
|
882
|
+
return {
|
|
883
|
+
snapshot: {
|
|
884
|
+
sessionId,
|
|
885
|
+
enabled: status.enabled,
|
|
886
|
+
lifetime: 'live-session',
|
|
887
|
+
...(pinned === undefined ? {} : { modelOverride: pinned }),
|
|
888
|
+
...(status.modelSource === undefined ? {} : { modelSource: status.modelSource }),
|
|
889
|
+
...(effective === undefined ? {} : { effectiveModel: effective }),
|
|
890
|
+
...(status.disabledReason === undefined ? {} : { disabledReason: status.disabledReason }),
|
|
891
|
+
},
|
|
892
|
+
};
|
|
893
|
+
};
|
|
894
|
+
const mapSetOutcome = (sessionId, outcome) => {
|
|
895
|
+
switch (outcome.kind) {
|
|
896
|
+
case 'committed':
|
|
897
|
+
case 'unchanged':
|
|
898
|
+
return snapshotOfStatus(sessionId, outcome.status);
|
|
899
|
+
case 'rejected':
|
|
900
|
+
return advisorSessionError('advisor/rejected', `advisor: ${outcome.reason}`);
|
|
901
|
+
case 'failed':
|
|
902
|
+
return advisorSessionError('advisor/failed', `advisor: model validation failed — previous selection untouched: ${outcome.reason}`);
|
|
903
|
+
case 'cancelled':
|
|
904
|
+
return advisorSessionError('advisor/cancelled', 'advisor: the model set was cancelled — previous selection untouched');
|
|
905
|
+
case 'superseded':
|
|
906
|
+
return advisorSessionError('advisor/superseded', 'advisor: superseded by a newer model command — previous selection untouched');
|
|
907
|
+
case 'gone':
|
|
908
|
+
return advisorSessionError('advisor/session-unknown', 'advisor: this session is no longer live — selection untouched');
|
|
909
|
+
}
|
|
910
|
+
};
|
|
911
|
+
const mapResetOutcome = (sessionId, outcome) => outcome.kind === 'reset' ? snapshotOfStatus(sessionId, outcome.status) : snapshotOfStatus(sessionId, sessionStatus(sessionId));
|
|
912
|
+
const sessionControlFace = {
|
|
913
|
+
getSession(sessionId) {
|
|
914
|
+
const unknown = sessionLiveError(sessionId);
|
|
915
|
+
if (unknown !== undefined)
|
|
916
|
+
return unknown;
|
|
917
|
+
return snapshotOfStatus(sessionId, sessionStatus(sessionId));
|
|
918
|
+
},
|
|
919
|
+
async setSessionModel(sessionId, provider, model) {
|
|
920
|
+
const unknown = sessionLiveError(sessionId);
|
|
921
|
+
if (unknown !== undefined)
|
|
922
|
+
return unknown;
|
|
923
|
+
const agent = ctx.agents.get(sessionId);
|
|
924
|
+
if (agent === undefined)
|
|
925
|
+
return sessionLiveError(sessionId);
|
|
926
|
+
// No per-request signal: the web RPC has no cancellation wire — the
|
|
927
|
+
// 60 s deadline and the owner-teardown signal (fused inside setModel)
|
|
928
|
+
// bound every lookup exactly like the command face's no-signal path.
|
|
929
|
+
return mapSetOutcome(sessionId, await controller.setModel(sessionId, provider, model, agent));
|
|
930
|
+
},
|
|
931
|
+
resetSessionModel(sessionId) {
|
|
932
|
+
const unknown = sessionLiveError(sessionId);
|
|
933
|
+
if (unknown !== undefined)
|
|
934
|
+
return unknown;
|
|
935
|
+
const agent = ctx.agents.get(sessionId);
|
|
936
|
+
if (agent === undefined)
|
|
937
|
+
return sessionLiveError(sessionId);
|
|
938
|
+
// Reset with no pin is a SUCCESS (`noop`) — the post-reset status
|
|
939
|
+
// reports the inheriting (possibly gate-blocked) state, matching the
|
|
940
|
+
// command face's truthfulness.
|
|
941
|
+
return mapResetOutcome(sessionId, controller.resetModel(sessionId, agent.session.seq));
|
|
942
|
+
},
|
|
943
|
+
};
|
|
944
|
+
sessionControl = sessionControlFace;
|
|
544
945
|
// T7: the command child activates ONLY when a command registry is composed
|
|
545
946
|
// (conditional child activation — `commands` must NOT join the top-level
|
|
546
947
|
// `inject` list, T1 fix). `/advisor` toggle/on/off/status (spec §2 S5).
|