dsh-advisor 0.1.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.
Files changed (46) hide show
  1. package/LICENSE +21 -0
  2. package/README.i18n.yaml +7 -0
  3. package/README.md +303 -0
  4. package/README.zh.md +166 -0
  5. package/cordis.patch.yml +6 -0
  6. package/lib/advisor-runtime.d.ts +242 -0
  7. package/lib/advisor-runtime.js +662 -0
  8. package/lib/advisor-runtime.js.map +1 -0
  9. package/lib/client/advisor-card.d.ts +90 -0
  10. package/lib/client/advisor-store.d.ts +310 -0
  11. package/lib/client/index.d.ts +39 -0
  12. package/lib/client/locales.d.ts +40 -0
  13. package/lib/client.d.ts +1 -0
  14. package/lib/client.js +840 -0
  15. package/lib/commands.d.ts +136 -0
  16. package/lib/commands.js +185 -0
  17. package/lib/commands.js.map +1 -0
  18. package/lib/config.d.ts +74 -0
  19. package/lib/config.js +93 -0
  20. package/lib/config.js.map +1 -0
  21. package/lib/delivery.d.ts +129 -0
  22. package/lib/delivery.js +169 -0
  23. package/lib/delivery.js.map +1 -0
  24. package/lib/emission-guard.d.ts +99 -0
  25. package/lib/emission-guard.js +155 -0
  26. package/lib/emission-guard.js.map +1 -0
  27. package/lib/gateway.d.ts +116 -0
  28. package/lib/gateway.js +214 -0
  29. package/lib/gateway.js.map +1 -0
  30. package/lib/index.d.ts +48 -0
  31. package/lib/index.js +485 -0
  32. package/lib/index.js.map +1 -0
  33. package/lib/kinds.d.ts +38 -0
  34. package/lib/kinds.js +24 -0
  35. package/lib/kinds.js.map +1 -0
  36. package/lib/prompts.d.ts +22 -0
  37. package/lib/prompts.js +38 -0
  38. package/lib/prompts.js.map +1 -0
  39. package/lib/settings.d.ts +96 -0
  40. package/lib/settings.js +141 -0
  41. package/lib/settings.js.map +1 -0
  42. package/lib/transcript.d.ts +257 -0
  43. package/lib/transcript.js +530 -0
  44. package/lib/transcript.js.map +1 -0
  45. package/package.json +90 -0
  46. package/scripts/build-client.mjs +268 -0
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Delivery routing (spec §2 S3, §4 mapping row, §6 delivery semantics,
3
+ * §8.4 KD-4) — the advice delivery channel into the primary agent.
4
+ *
5
+ * One {@link AdvisorDelivery} exists per plugin load and owns:
6
+ *
7
+ * - **The KD-4 per-session agent map**: keyed by `agent.id` (=== `session.id`),
8
+ * maintained by `index.ts` on `agent/created` / `agent/disposed`, with a
9
+ * registry fallback (`ctx.agents.get(session.id)`, injected as
10
+ * `lookupAgent`) that covers agents published before this plugin loaded.
11
+ * A missing agent at delivery time drops the note with a log — advisory
12
+ * only, never throw, never stall.
13
+ * - **Severity routing (spec §6)**: nit → `agent.inject` (non-waking, consumed
14
+ * at the next pre-step boundary); concern/blocker → `agent.steer` (waking —
15
+ * an idle driver starts a turn, a running driver consumes at its next step
16
+ * boundary).
17
+ * - **The immuneTurns cooldown (spec §6)**: after a concern/blocker is actually
18
+ * steered, the next `immuneTurns` stepped primary turns must complete before
19
+ * another interrupting note may steer; interrupting notes inside the window
20
+ * downgrade to inject. The fence arms only on a real steer delivery; the
21
+ * observer's `onSteppedTurnEnd` / `onRewrite` hooks (T3 wiring) drive the
22
+ * countdown and the KD-5 reset.
23
+ *
24
+ * Message shape (spec §6): a user-role message via `createUserMessage` whose
25
+ * source carries the distinct `kind === 'advisor'` (the plugin's
26
+ * `MessageSourceMap` merge extension, src/kinds.ts) and whose content is
27
+ * self-describing `[advisor:{severity}] {note}` — the only cue the primary
28
+ * model gets about how to treat it ("weigh, don't blindly obey" spirit).
29
+ *
30
+ * Delivery is synchronous and fire-and-forget; the runtime path (T4 F1) is
31
+ * what contains a throwing `inject`/`steer` — this module lets agent-method
32
+ * throws propagate to that containment seam.
33
+ *
34
+ * @module dsh-advisor/delivery
35
+ */
36
+ import { boundContextSummary, createUserMessage } from '@deepseek-ai/dsh-llm';
37
+ import { ADVISOR_SOURCE_KIND } from './kinds.js';
38
+ /**
39
+ * Build the advisor message for one note (spec §6): a user-role message whose
40
+ * source carries the distinct advisor kind and whose content is self-describing
41
+ * `[advisor:{severity}] {note}`.
42
+ *
43
+ * Bounds (qc3 F-2 / qc2 S-1): the note itself is already capped at
44
+ * `ADVISOR_NOTE_MAX_CHARS` by extraction; the collapsed-row summary is
45
+ * additionally bounded via `boundContextSummary` (120 chars — the platform
46
+ * `CONTEXT_SUMMARY_MAX_CHARS` convention), so the durable log and the
47
+ * collapsed context row never carry an unbounded account.
48
+ */
49
+ export function buildAdvisorMessage(note) {
50
+ // One formatter for content and summary: the summary is derived from the
51
+ // same string as the content via a fixed slice of the `[advisor:` marker
52
+ // (`[` + everything after it), so the severity formatting can never drift
53
+ // between the two — and note text mentioning "advisor:" cannot affect it.
54
+ const text = `[advisor:${note.severity}] ${note.note}`;
55
+ const summary = `[${text.slice('[advisor:'.length)}`;
56
+ return createUserMessage({
57
+ content: [{ type: 'text', text }],
58
+ // n4 (user direction): declare the notice form + a collapsed-row summary so
59
+ // the web shell's ContextInjectionRow shows "… · advisor · [nit] <note>"
60
+ // instead of a bare producer label. The severity tag is part of the summary
61
+ // text; per-severity COLOR needs the shell UI (out of plugin reach).
62
+ source: {
63
+ kind: ADVISOR_SOURCE_KIND,
64
+ form: 'notice',
65
+ summary: boundContextSummary(summary),
66
+ },
67
+ });
68
+ }
69
+ /** True when a note interrupts the primary (concern/blocker — spec §6). */
70
+ function isInterrupting(severity) {
71
+ return severity === 'concern' || severity === 'blocker';
72
+ }
73
+ /**
74
+ * Per-plugin delivery router: severity → channel, the KD-4 agent map, and the
75
+ * immuneTurns cooldown. Cordis-free, so the routing logic is unit-testable.
76
+ */
77
+ export class AdvisorDelivery {
78
+ immuneTurns;
79
+ lookupAgent;
80
+ logger;
81
+ /** KD-4 per-session agent map, keyed by `agent.id` (=== session.id). */
82
+ agents = new Map();
83
+ /**
84
+ * immuneTurns latch: remaining stepped primary turns before an interrupting
85
+ * note may steer again (spec §6). A present entry > 0 means armed; the entry
86
+ * is removed when the countdown exhausts.
87
+ */
88
+ cooldown = new Map();
89
+ constructor(options) {
90
+ this.immuneTurns = options.immuneTurns;
91
+ this.lookupAgent = options.lookupAgent ?? (() => undefined);
92
+ this.logger = options.logger ?? console;
93
+ }
94
+ /** KD-4: register an agent on `agent/created` (keyed by `agent.id`). */
95
+ registerAgent(agent) {
96
+ this.agents.set(agent.id, agent);
97
+ }
98
+ /** KD-4: drop an agent — and its cooldown with the session — on `agent/disposed`. */
99
+ unregisterAgent(sessionId) {
100
+ this.agents.delete(sessionId);
101
+ this.cooldown.delete(sessionId);
102
+ }
103
+ /**
104
+ * Update the immuneTurns cooldown length (live config — settings onChange,
105
+ * plan dsh-advisor-settings-n2 T1). The fence is re-armed with the new
106
+ * length on the next real steer; the per-session cooldown countdown itself
107
+ * is untouched, so the delivery semantics (spec §6) never change mid-window.
108
+ */
109
+ setImmuneTurns(value) {
110
+ this.immuneTurns = value;
111
+ }
112
+ /**
113
+ * One completed stepped primary turn (observer `onSteppedTurnEnd`): decrement
114
+ * the immuneTurns countdown. The latch is removed at zero, so the next
115
+ * interrupting note steers again. Total — never throws.
116
+ */
117
+ onSteppedTurnEnd(sessionId) {
118
+ const remaining = this.cooldown.get(sessionId);
119
+ if (remaining === undefined || remaining <= 0)
120
+ return;
121
+ if (remaining <= 1)
122
+ this.cooldown.delete(sessionId);
123
+ else
124
+ this.cooldown.set(sessionId, remaining - 1);
125
+ }
126
+ /**
127
+ * KD-5 reset trigger: a compaction / surface rewrite clears the immuneTurns
128
+ * latch — the session state is being rewritten, so the cooldown's turn-count
129
+ * basis no longer applies. Total — never throws.
130
+ */
131
+ reset(sessionId) {
132
+ this.cooldown.delete(sessionId);
133
+ }
134
+ /**
135
+ * Route one accepted advice note (spec §6, KD-4).
136
+ *
137
+ * Resolves the primary agent via the map, falling back to the registry; a
138
+ * missing agent drops the note with a log (advisory only — never throw,
139
+ * never stall). nit → inject; concern/blocker → steer, unless the
140
+ * immuneTurns fence is armed, in which case they downgrade to inject.
141
+ *
142
+ * @returns the channel delivered on, or `undefined` when dropped (no agent).
143
+ */
144
+ route(sessionId, note) {
145
+ const agent = this.agents.get(sessionId) ?? this.lookupAgent(sessionId);
146
+ if (agent === undefined) {
147
+ this.logger.warn('advisor: note dropped — no agent for session', {
148
+ sessionId,
149
+ severity: note.severity,
150
+ });
151
+ return undefined;
152
+ }
153
+ const message = buildAdvisorMessage(note);
154
+ const cooldown = this.cooldown.get(sessionId) ?? 0;
155
+ if (isInterrupting(note.severity) && cooldown <= 0) {
156
+ // A real steer delivery arms the fence (spec §6). Armed before the call:
157
+ // a failed steer delivery still counts as an attempted interrupt, keeping
158
+ // a throwing agent out of the noise loop — and a downgraded or dropped
159
+ // note never reaches this branch, so the fence never arms on one.
160
+ if (this.immuneTurns > 0)
161
+ this.cooldown.set(sessionId, this.immuneTurns);
162
+ agent.steer(message);
163
+ return 'steer';
164
+ }
165
+ agent.inject(message);
166
+ return 'inject';
167
+ }
168
+ }
169
+ //# sourceMappingURL=delivery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delivery.js","sourceRoot":"","sources":["../src/delivery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Emission guard (spec §4 mapping row — omp `advisor/emission-guard.ts`
3
+ * ported 1:1: normalize / dedupe / content-free suppression / one-per-update
4
+ * / escalation; §6 one-note-per-update; §8.2 KD-2 "guard also drops extras").
5
+ *
6
+ * The guard sits between the T4 runtime (note extraction) and delivery (T6):
7
+ * {@link EmissionGuard.accept} returns `true` to pass a note through to the
8
+ * delivery callback and `false` to suppress it **silently** — the caller can
9
+ * never tell an accepted from a suppressed note, and no error is thrown.
10
+ *
11
+ * Rules (in order):
12
+ * 1. **Normalization** — lowercase, NFKC, every run of non-alphanumeric
13
+ * characters collapsed to one space, trimmed. `"Stop."`, `*stop*` and
14
+ * `" STOP "` all key to `stop`.
15
+ * 2. **Content-free phrase filter** — short phrases with no concrete reason
16
+ * (`stop`, `done`, `complete`, `no issue continue`, `lgtm`,
17
+ * `nothing to add`, plus documented equivalents — see
18
+ * {@link CONTENT_FREE_PHRASES}) are suppressed. Matching is exact on the
19
+ * normalized text, so a note that merely contains a phrase survives.
20
+ * 3. **Per-update rate limit** — at most one note per advisor model cycle is
21
+ * accepted. The runtime signals each new cycle with {@link
22
+ * EmissionGuard.beginUpdate} (once per processed delta), resetting the
23
+ * latch. A guard whose `beginUpdate` is never called still accepts the
24
+ * first note of the session (fail-safe toward fewer notes).
25
+ * 4. **Normalized dedupe with escalation** — a normalized note already
26
+ * accepted this session is dropped, with a FIFO-bounded history
27
+ * (default 4096, omp parity; {@link DEFAULT_MAX_HISTORY}); the oldest
28
+ * entry is evicted past the bound. A repeat at equal or lower severity is
29
+ * suppressed, but a real escalation (nit → concern → blocker) of the same
30
+ * note is accepted and updates the remembered severity.
31
+ *
32
+ * Guard state clears per session: a new runtime (and therefore a new guard)
33
+ * is created per session and discarded on dispose; {@link
34
+ * EmissionGuard.reset} is exposed for the KD-5 session-reset triggers
35
+ * (`compact/*`, `user/message` replace) when the reset wiring lands.
36
+ *
37
+ * @module dsh-advisor/emission-guard
38
+ */
39
+ import type { AdviceNote } from './advisor-runtime.js';
40
+ /**
41
+ * FIFO dedupe history bound (omp parity: 4096). Deliberately capped — a
42
+ * session that accepts more than this many distinct notes keeps only the most
43
+ * recent window, so an old note may legitimately be advised again.
44
+ */
45
+ export declare const DEFAULT_MAX_HISTORY = 4096;
46
+ /**
47
+ * Content-free phrases suppressed at delivery. The omp base list is
48
+ * `stop` / `done` / `complete` / `no issue continue` / `lgtm` /
49
+ * `nothing to add` (this is also the frame the KD-2 prompt asks the model to
50
+ * emit when there is nothing to advise). Additions beyond the omp base —
51
+ * short acknowledgements with no concrete reason — are listed after them.
52
+ */
53
+ export declare const CONTENT_FREE_PHRASES: ReadonlySet<string>;
54
+ /** Options for one {@link EmissionGuard}. */
55
+ export interface EmissionGuardOptions {
56
+ /**
57
+ * FIFO dedupe history bound (normalized notes remembered per session);
58
+ * default {@link DEFAULT_MAX_HISTORY} (4096, omp parity).
59
+ */
60
+ readonly maxHistory?: number;
61
+ }
62
+ /**
63
+ * Per-session emission guard. One instance per advisor runtime; created via
64
+ * {@link createEmissionGuard} (or the constructor directly).
65
+ */
66
+ export declare class EmissionGuard {
67
+ private readonly maxHistory;
68
+ /**
69
+ * normalized note → last accepted severity. Map insertion order is the
70
+ * FIFO order (updating an existing key does not move it), so the oldest
71
+ * entry is simply the first key.
72
+ */
73
+ private readonly history;
74
+ /** One-note-per-update latch; reset by {@link beginUpdate} and {@link reset}. */
75
+ private acceptedThisUpdate;
76
+ constructor(options?: EmissionGuardOptions);
77
+ /**
78
+ * Mark the start of one advisor model cycle (one processed transcript
79
+ * delta). The per-update rate limit latch resets here, so each cycle may
80
+ * deliver one note again. The runtime calls this once per delta it drains.
81
+ */
82
+ beginUpdate(): void;
83
+ /**
84
+ * Accept or suppress one extracted note.
85
+ *
86
+ * @returns `true` = pass through to delivery (T6); `false` = suppressed
87
+ * (normalized-empty, content-free, a repeat at equal/lower severity, or
88
+ * the second note of the same update). Never throws.
89
+ */
90
+ accept(note: AdviceNote): boolean;
91
+ /**
92
+ * Clear all session state (dedupe history + per-update latch). Exposed for
93
+ * the KD-5 session-reset triggers; a new session gets a fresh guard anyway
94
+ * (per-runtime lifetime).
95
+ */
96
+ reset(): void;
97
+ }
98
+ /** Factory (plan T5: "Export the guard class + a factory"). */
99
+ export declare function createEmissionGuard(options?: EmissionGuardOptions): EmissionGuard;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Emission guard (spec §4 mapping row — omp `advisor/emission-guard.ts`
3
+ * ported 1:1: normalize / dedupe / content-free suppression / one-per-update
4
+ * / escalation; §6 one-note-per-update; §8.2 KD-2 "guard also drops extras").
5
+ *
6
+ * The guard sits between the T4 runtime (note extraction) and delivery (T6):
7
+ * {@link EmissionGuard.accept} returns `true` to pass a note through to the
8
+ * delivery callback and `false` to suppress it **silently** — the caller can
9
+ * never tell an accepted from a suppressed note, and no error is thrown.
10
+ *
11
+ * Rules (in order):
12
+ * 1. **Normalization** — lowercase, NFKC, every run of non-alphanumeric
13
+ * characters collapsed to one space, trimmed. `"Stop."`, `*stop*` and
14
+ * `" STOP "` all key to `stop`.
15
+ * 2. **Content-free phrase filter** — short phrases with no concrete reason
16
+ * (`stop`, `done`, `complete`, `no issue continue`, `lgtm`,
17
+ * `nothing to add`, plus documented equivalents — see
18
+ * {@link CONTENT_FREE_PHRASES}) are suppressed. Matching is exact on the
19
+ * normalized text, so a note that merely contains a phrase survives.
20
+ * 3. **Per-update rate limit** — at most one note per advisor model cycle is
21
+ * accepted. The runtime signals each new cycle with {@link
22
+ * EmissionGuard.beginUpdate} (once per processed delta), resetting the
23
+ * latch. A guard whose `beginUpdate` is never called still accepts the
24
+ * first note of the session (fail-safe toward fewer notes).
25
+ * 4. **Normalized dedupe with escalation** — a normalized note already
26
+ * accepted this session is dropped, with a FIFO-bounded history
27
+ * (default 4096, omp parity; {@link DEFAULT_MAX_HISTORY}); the oldest
28
+ * entry is evicted past the bound. A repeat at equal or lower severity is
29
+ * suppressed, but a real escalation (nit → concern → blocker) of the same
30
+ * note is accepted and updates the remembered severity.
31
+ *
32
+ * Guard state clears per session: a new runtime (and therefore a new guard)
33
+ * is created per session and discarded on dispose; {@link
34
+ * EmissionGuard.reset} is exposed for the KD-5 session-reset triggers
35
+ * (`compact/*`, `user/message` replace) when the reset wiring lands.
36
+ *
37
+ * @module dsh-advisor/emission-guard
38
+ */
39
+ /**
40
+ * FIFO dedupe history bound (omp parity: 4096). Deliberately capped — a
41
+ * session that accepts more than this many distinct notes keeps only the most
42
+ * recent window, so an old note may legitimately be advised again.
43
+ */
44
+ export const DEFAULT_MAX_HISTORY = 4096;
45
+ /**
46
+ * Content-free phrases suppressed at delivery. The omp base list is
47
+ * `stop` / `done` / `complete` / `no issue continue` / `lgtm` /
48
+ * `nothing to add` (this is also the frame the KD-2 prompt asks the model to
49
+ * emit when there is nothing to advise). Additions beyond the omp base —
50
+ * short acknowledgements with no concrete reason — are listed after them.
51
+ */
52
+ export const CONTENT_FREE_PHRASES = new Set([
53
+ // omp base list
54
+ 'stop',
55
+ 'done',
56
+ 'complete',
57
+ 'no issue continue',
58
+ 'lgtm',
59
+ 'nothing to add',
60
+ // additions (documented in the task-5 report): content-free equivalents
61
+ 'ok',
62
+ 'okay',
63
+ 'good',
64
+ 'fine',
65
+ 'looks good',
66
+ 'looks fine',
67
+ 'all good',
68
+ 'all clear',
69
+ 'no issue',
70
+ 'no issues',
71
+ 'nothing',
72
+ 'looks good to me',
73
+ ]);
74
+ /** Severity order for the escalation check (spec §6): nit < concern < blocker. */
75
+ const SEVERITY_RANK = { nit: 0, concern: 1, blocker: 2 };
76
+ /**
77
+ * Normalize one note to its identity key (spec §4): lowercase, NFKC, every
78
+ * run of non-alphanumeric characters collapsed to one space, trimmed.
79
+ */
80
+ function normalizeNote(text) {
81
+ return text
82
+ .normalize('NFKC')
83
+ .toLowerCase()
84
+ .replace(/[^\p{L}\p{N}]+/gu, ' ')
85
+ .trim();
86
+ }
87
+ /**
88
+ * Per-session emission guard. One instance per advisor runtime; created via
89
+ * {@link createEmissionGuard} (or the constructor directly).
90
+ */
91
+ export class EmissionGuard {
92
+ maxHistory;
93
+ /**
94
+ * normalized note → last accepted severity. Map insertion order is the
95
+ * FIFO order (updating an existing key does not move it), so the oldest
96
+ * entry is simply the first key.
97
+ */
98
+ history = new Map();
99
+ /** One-note-per-update latch; reset by {@link beginUpdate} and {@link reset}. */
100
+ acceptedThisUpdate = false;
101
+ constructor(options) {
102
+ this.maxHistory = options?.maxHistory ?? DEFAULT_MAX_HISTORY;
103
+ }
104
+ /**
105
+ * Mark the start of one advisor model cycle (one processed transcript
106
+ * delta). The per-update rate limit latch resets here, so each cycle may
107
+ * deliver one note again. The runtime calls this once per delta it drains.
108
+ */
109
+ beginUpdate() {
110
+ this.acceptedThisUpdate = false;
111
+ }
112
+ /**
113
+ * Accept or suppress one extracted note.
114
+ *
115
+ * @returns `true` = pass through to delivery (T6); `false` = suppressed
116
+ * (normalized-empty, content-free, a repeat at equal/lower severity, or
117
+ * the second note of the same update). Never throws.
118
+ */
119
+ accept(note) {
120
+ const key = normalizeNote(note.note);
121
+ if (key.length === 0)
122
+ return false; // punctuation-only / whitespace-only
123
+ if (CONTENT_FREE_PHRASES.has(key))
124
+ return false;
125
+ if (this.acceptedThisUpdate)
126
+ return false; // one note per update
127
+ const prior = this.history.get(key);
128
+ if (prior !== undefined && SEVERITY_RANK[note.severity] <= SEVERITY_RANK[prior]) {
129
+ return false; // equal/lower-severity repeat suppressed
130
+ }
131
+ this.history.set(key, note.severity); // new note, or an accepted escalation
132
+ if (this.history.size > this.maxHistory) {
133
+ // FIFO eviction: drop the oldest accepted note past the bound.
134
+ const oldest = this.history.keys().next().value;
135
+ if (oldest !== undefined)
136
+ this.history.delete(oldest);
137
+ }
138
+ this.acceptedThisUpdate = true;
139
+ return true;
140
+ }
141
+ /**
142
+ * Clear all session state (dedupe history + per-update latch). Exposed for
143
+ * the KD-5 session-reset triggers; a new session gets a fresh guard anyway
144
+ * (per-runtime lifetime).
145
+ */
146
+ reset() {
147
+ this.history.clear();
148
+ this.acceptedThisUpdate = false;
149
+ }
150
+ }
151
+ /** Factory (plan T5: "Export the guard class + a factory"). */
152
+ export function createEmissionGuard(options) {
153
+ return new EmissionGuard(options);
154
+ }
155
+ //# sourceMappingURL=emission-guard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emission-guard.js","sourceRoot":"","sources":["../src/emission-guard.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,116 @@
1
+ /**
2
+ * T1 (plan dsh-advisor-settings-gateway-n5) — host-side `advisor` config
3
+ * gateway: the `/api/advisor/get` + `/api/advisor/set` Remote endpoints.
4
+ *
5
+ * Transport: the typertGateway `/api` interceptor is the single host-wide RPC
6
+ * slot (a plugin must NOT `connection.rpc.intercept('/api')` again — it would
7
+ * throw). Instead this service declares a typertGateway binding (via the
8
+ * `TypertRemoteService` base) plus `@Remote` method markers; the gateway's SRC
9
+ * discovery (`claimsEndpoint` — `ctx.reflect.props` + `remoteMethods`) claims
10
+ * `/api/advisor/get` and `/api/advisor/set`, and the payload contract is
11
+ * exactly one plain-object `args` field whose keys are the method parameter
12
+ * names (`get()` → `{ args: {} }`; `set(patch)` → `{ args: { patch } }`).
13
+ *
14
+ * Data: `get` reads the `AdvisorSettingsBridge` source — the same live
15
+ * composed config the runtime reads (schema defaults → plugin-row base →
16
+ * settings user layer), resolved through the `resolveAdvisorConfig` hard gate
17
+ * (the SSOT for enabled-without-pair disabled-with-reason). `set` validates
18
+ * the patch against the `Config` schema first (unknown-key rejection
19
+ * unchanged — the settings service itself is non-strict and would accept the
20
+ * unknown key), then writes the USER layer in-process via
21
+ * `ctx.settings.update` (no exposed-namespace gate on the in-process write —
22
+ * the wire-level `exposedNamespaces()` check only guards the apiproxy path),
23
+ * and returns the new composed value.
24
+ *
25
+ * The settings service is OPTIONAL (no settings service → the bridge source
26
+ * stays the entry, `get` still works; `set` fails with a clear error — KD-G5
27
+ * fallback). The gateway captures the service through a conditional
28
+ * `ctx.inject(['settings'], ...)` child (the same activation pattern as
29
+ * `installAdvisorSettings`), because `ctx.settings` is only resolvable from a
30
+ * fiber that declares it.
31
+ *
32
+ * The returned config is normalized to the typertGateway JSON wire boundary:
33
+ * absent keys (provider/model/disabledReason) are OMITTED, never
34
+ * present-as-undefined (the gateway's result validation rejects undefined
35
+ * values).
36
+ *
37
+ * @module dsh-advisor/gateway
38
+ */
39
+ import type { Context } from '@deepseek-ai/cordis';
40
+ import type { TypertContribution } from '@deepseek-ai/dsh-typert-registry';
41
+ import { TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol';
42
+ import type { AdvisorSettingsBridge } from './settings.js';
43
+ import type { AdvisorConfig, ResolvedAdvisorConfig } from './config.js';
44
+ /** Patch shape accepted by `advisor.set` — any subset of the config keys. */
45
+ export type AdvisorConfigPatch = Partial<AdvisorConfig>;
46
+ /**
47
+ * The host-side `advisor` config gateway (`/api/advisor/get` +
48
+ * `/api/advisor/set`). Registered as the cordis service key `'advisor'`
49
+ * (namespace defaults to the service key). The `TypertRemoteService` base is
50
+ * kept ONLY for its `typertRemote` binding — the typertGateway's dispatch
51
+ * `validateBinding` requires the visible binding on the live service (a pure
52
+ * instance property, no module-private state). Endpoints are registered
53
+ * EXPLICITLY through `ctx.typert.register(advisorTypertContribution())`
54
+ * (see `apply` in `src/index.ts`) instead of the `@Remote` SRC markers:
55
+ * SRC discovery reads `remoteMethods()` — a module-private WeakMap in
56
+ * `@deepseek-ai/dsh-typert-protocol` — so a locally-linked plugin whose
57
+ * peers resolve outside the host installation never shares that table with
58
+ * the host typertGateway (zero claimed endpoints, `/api/advisor/*` 404).
59
+ * The explicit `TypertRegistry.register` path writes the invocation
60
+ * descriptors into `ctx.typert.local`, which `claimsEndpoint` checks FIRST,
61
+ * so claim + dispatch work regardless of module identity.
62
+ */
63
+ export declare class AdvisorConfigGateway extends TypertRemoteService {
64
+ private readonly bridge;
65
+ /** The live settings service once the optional inject child activates. */
66
+ private settings;
67
+ /**
68
+ * @param ctx - owning context (the plugin fiber's ctx inside `apply`).
69
+ * @param bridge - the same `AdvisorSettingsBridge` the runtime reads, so
70
+ * get/set always operate on the live composed config.
71
+ */
72
+ constructor(ctx: Context, bridge: AdvisorSettingsBridge);
73
+ /**
74
+ * Read the current composed config (schema defaults → entry base → settings
75
+ * user layer) through the hard gate.
76
+ * @returns the resolved config (incl. disabledReason when the gate blocks).
77
+ */
78
+ get(): {
79
+ config: ResolvedAdvisorConfig;
80
+ };
81
+ /**
82
+ * Validate a config patch and write it to the settings USER layer (live —
83
+ * the runtime re-applies through the bridge `onChange`; no restart needed).
84
+ * @param patch - any subset of the config keys; unknown keys are rejected
85
+ * by the `Config` schema before anything is written.
86
+ * @returns the NEW composed config after the write.
87
+ * @throws when the patch fails `Config` validation, or when no settings
88
+ * service is composed (KD-G5: the write channel is unavailable).
89
+ */
90
+ set(patch: AdvisorConfigPatch): Promise<{
91
+ config: ResolvedAdvisorConfig;
92
+ }>;
93
+ /**
94
+ * Resolve the live composed config through the hard gate. Containment
95
+ * (qc2 W-1): a user layer the resolver rejects (e.g. an unknown key that
96
+ * survived the non-strict settings schema) resolves to disabled-with-reason
97
+ * carrying the message — the gateway never fails the RPC on a bad user
98
+ * layer, and gate semantics hold (no model call can start). S1: when the
99
+ * raw source is still readable, the fallback seeds its scalar latches
100
+ * (systemPrompt / immuneTurns / maxDeltaMessages) instead of hardcoded
101
+ * defaults, so an invalid layer only drops the offending keys.
102
+ */
103
+ private readConfig;
104
+ }
105
+ /**
106
+ * The explicit typert contribution for the `advisor` gateway endpoints —
107
+ * registered via `ctx.typert.register(...)` (see `apply` in `src/index.ts`).
108
+ * The descriptors mirror exactly what the former SRC discovery derived from
109
+ * the `@Remote` markers (`src:advisor#<endpoint>` identity shape, direct
110
+ * receiver, JSON wire params with `src-json` codec), so the host
111
+ * typertGateway claim + dispatch behavior is byte-for-byte the same — the
112
+ * only difference is the registration does not depend on the module-private
113
+ * `remoteMethods` marker table, which a locally-linked plugin can never
114
+ * share with the host installation.
115
+ */
116
+ export declare function advisorTypertContribution(): TypertContribution;