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 CHANGED
@@ -3,5 +3,5 @@
3
3
  # editing either side, bring the other along and re-record with:
4
4
  # git hash-object README.md
5
5
  # git hash-object README.zh.md
6
- README.md: 2d4baa3b701e2a22297123fc100f706230d8078a
7
- README.zh.md: 7efa6d1782fb9a019340d900196e35f4982da79a
6
+ README.md: ce87847dc44cf80fe0afe72fb7d166eef9e8084e
7
+ README.zh.md: 4092cedf85c5ad7643a772b1ac2df6388c4f63a6
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm](https://img.shields.io/npm/dt/dsh-advisor)](https://www.npmjs.com/package/dsh-advisor)
6
6
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
7
  ![node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933.svg)
8
- ![dsh](https://img.shields.io/badge/dsh-0.1.5--rc.1-4B32C3.svg)
8
+ ![dsh](https://img.shields.io/badge/dsh-0.1.7--rc.1-4B32C3.svg)
9
9
  ![dsh tui](https://img.shields.io/badge/dsh%20tui-compatible-4B32C3.svg)
10
10
  [![dshfind](https://dshfind.com/api/badge/omdsh-dev/dsh-advisor)](https://dshfind.com/plugins/omdsh-dev/dsh-advisor?ref=badge)
11
11
 
@@ -13,7 +13,7 @@ A standalone dsh (DeepSeek Harness) plugin bundle porting the omp "advisor" subs
13
13
 
14
14
  **Advisory only.** The advisor never approves or rejects the primary agent's actions, and never issues commands as if it were the primary agent. Every delivered message is self-described advisory content, and a misbehaving reviewer is bounded end to end (emission guard, immuneTurns cooldown, failure policy) so it can never stall or pollute the primary loop.
15
15
 
16
- Works in both dsh front ends: the **web** profile (Settings → 插件配置 → Advisor card) and the **dsh-tui** terminal profile (`/advisor` + `/advisor config`).
16
+ Works in both dsh front ends: the **web** profile (sidebar → Plugins → dsh-advisor → Advisor card) and the **dsh-tui** terminal profile (`/advisor` + `/advisor config`).
17
17
 
18
18
  ## Quick start
19
19
 
@@ -28,29 +28,31 @@ Same plugin, either front end — the only difference is the `--profile` flag. P
28
28
 
29
29
  ### Configuration
30
30
 
31
- Add an `advisor:` section to the global dsh settings document (default `$DSH_HOME/settings.yaml` — shared across profiles; the web Settings card writes to this same file):
31
+ Edit the `config` of the `advisor` row in your profile's patch layer (`~/.dsh/profiles/<profile>/cordis.patch.yml`). All six fields are schema-volatile live fields (dsh ≥ 0.1.7-rc.1): the web card and the TUI `/settings` screen write this same entry config — persisted in the profile patch, committed without a remount. (A pre-0.1.7 `$DSH_HOME/settings.yaml` `advisor:` section no longer exists: dsh imports it into the active profile once and renames the file `.imported`.)
32
32
 
33
33
  ```yaml
34
- advisor:
35
- enabled: true # master switch (default false) — set explicitly to enable
36
- provider: deepseek-official # REQUIRED when enabled
37
- model: deepseek-flash # REQUIRED when enabled; fallback: deepseek-v4-flash (or another V4 id) until the gateway enables the V41 route
38
- systemPrompt: "" # optional; "" = built-in reviewer prompt
39
- immuneTurns: 3 # int ≥ 0, default 3 — cooldown after a delivered steer
40
- maxDeltaMessages: 60 # int ≥ 0, default 60 — delta window; 0 = unbounded
34
+ # ~/.dsh/profiles/<profile>/cordis.patch.yml — the advisor row's config
35
+ - id: advisor
36
+ config:
37
+ enabled: true # master switch (default false) — set explicitly to enable
38
+ provider: deepseek-official # REQUIRED when enabled
39
+ model: deepseek-flash # REQUIRED when enabled; fallback: deepseek-v4-flash (or another V4 id) until the gateway enables the V41 route
40
+ systemPrompt: "" # optional; "" = built-in reviewer prompt
41
+ immuneTurns: 3 # int ≥ 0, default 3 — cooldown after a delivered steer
42
+ maxDeltaMessages: 60 # int ≥ 0, default 60 — delta window; 0 = unbounded
41
43
  ```
42
44
 
43
45
  The advisor is off by default. When enabled, `provider` and `model` are **mandatory**: `enabled: true` without both is a hard gate — the advisor never starts a model call and reports a disabled-with-reason status; unknown config keys are rejected.
44
46
 
45
- The same keys compose across **three surfaces** (later layers override earlier ones; every surface shares the same key set and the same hard gate, with the host-side gate as the final line of defense on every path):
47
+ The same keys are read and edited from **three surfaces** (one store — the advisor entry config above; every surface shares the same key set and the same hard gate, with the host-side gate as the final line of defense on every path):
46
48
 
47
- 1. **Plugin-row config** — the profile patch layer (`$DSH_HOME/profiles/<profile>/cordis.patch.yml`). This is the composition base.
48
- 2. **dsh web Settings page — the "插件配置" (Plugin Configuration) page** — the Advisor **card** (namespace key `advisor`) with the enabled toggle, provider / model selects restricted to system-configured providers and their models, and the optional fields. Saving writes into the `advisor` settings namespace and applies to new sessions immediately — no restart. The card requires a current dsh web build whose shell declares the `settings.plugin.item` card slot and loads packages that declare `dsh.client`; it reads and writes the namespace through the official `GatewayService` RPC channel (`/api/advisor/get` + `/api/advisor/set`), which is not gated by the settings exposure allowlist. It additionally blocks saving while enabled with a required field empty.
49
- 3. **`/advisor` command** — per-session and ephemeral: it flips a session override, never the persisted config (see [Verify](#verify)).
49
+ 1. **Plugin-row config** — the profile patch layer (`~/.dsh/profiles/<profile>/cordis.patch.yml`). This is where the config lives.
50
+ 2. **dsh web Plugins page — the dsh-advisor bundle's own page** — the Advisor **card** (bundle key `dsh-advisor`) with the enabled toggle, provider / model selects restricted to system-configured providers and their models, and the optional fields. Saving writes the advisor entry's config (landed through the config editor into the profile patch) and applies to running sessions immediately — no restart. The card requires a dsh web build whose shell declares the `plugins.bundle.config` card slot (dsh ≥ 0.1.7-rc.1) and loads packages that declare `dsh.client`; it reads and writes the config through the official `GatewayService` RPC channel (`/api/advisor/get` + `/api/advisor/set`), which is not gated by the settings exposure allowlist. It additionally blocks saving while enabled with a required field empty.
51
+ 3. **`/advisor` command** — per-session and ephemeral: it flips a session override and pins a per-session reviewer model, never the persisted config (see [Verify](#verify)).
50
52
 
51
- In a **dsh-tui** profile the same five keys are editable in the TUI `/settings` screen: run `dsh --profile dsh-tui`, open `/settings`, and edit the **Advisor** section (`enabled` / `provider` / `model` / `immuneTurns` / `maxDeltaMessages`, each with zh/en label + hint). Edits are staged and written on save through the revision-fenced `settings.mutate` into the same `advisor` namespace user layer the web card writes, and re-apply live without a restart. `systemPrompt` is NOT a TUI field (the TUI text control is single-line; a multi-line prompt would be truncated) — edit it via the web card or `$DSH_HOME/settings.yaml`. The section requires dsh-tui ≥ v0.8.0 (shipped in the `dsh-tui-settings-sections` row of the v0.8.0+ bundle); older dsh-tui versions no-op it cleanly and the two file paths — profile patch layer + global `$DSH_HOME/settings.yaml` — remain the edit paths. `/advisor config` stays a read-only readback whose edit hint names the `/settings` screen when the seam is mounted. Save behavior differs from the web card: the TUI seam has no cross-field validation, so a save may set `enabled: true` with empty `provider`/`model` — the explicit model gate resolves that to disabled-with-reason at runtime (visible via `/advisor status` and `/advisor config`); the web card blocks such a save outright. Full reference → [docs/configuration.md](docs/configuration.md).
53
+ In a **dsh-tui** profile the same five keys are editable in the TUI `/settings` screen: run `dsh --profile dsh-tui`, open `/settings`, and edit the **Advisor** section (`enabled` / `provider` / `model` / `immuneTurns` / `maxDeltaMessages`, each with zh/en label + hint). Edits are staged and written on save through the revision-fenced `settings.mutate` into the same advisor entry config the web card writes, and re-apply live without a restart. `systemPrompt` is NOT a TUI field (the TUI text control is single-line; a multi-line prompt would be truncated) — edit it via the web card or the profile patch layer. The section requires dsh-tui ≥ v0.8.0 (shipped in the `dsh-tui-settings-sections` row of the v0.8.0+ bundle); older dsh-tui versions no-op it cleanly and the profile patch layer remains the edit path. `/advisor config` stays a read-only readback whose edit hint names the `/settings` screen when the seam is mounted. Save behavior differs from the web card: the TUI seam has no cross-field validation, so a save may set `enabled: true` with empty `provider`/`model` — the explicit model gate resolves that to disabled-with-reason at runtime (visible via `/advisor status` and `/advisor config`); the web card blocks such a save outright. Full reference → [docs/configuration.md](docs/configuration.md).
52
54
 
53
- ![Advisor card on the dsh web Settings (插件配置) page](docs/screenshots/advisor-settings-card.webp)
55
+ ![Advisor card on the dsh web Plugins page (the dsh-advisor bundle page)](docs/screenshots/advisor-settings-card.webp)
54
56
 
55
57
  ### Verify
56
58
 
@@ -65,31 +67,38 @@ With the advisor installed and enabled, control it in-session with the `/advisor
65
67
  /advisor on enable the advisor for this session
66
68
  /advisor off disable the advisor for this session
67
69
  /advisor status show state, model, runtime status, pending count, last activity
70
+ /advisor model show the effective reviewer model and its source (session override or global default)
71
+ /advisor model set <provider> <model> pin a reviewer model for this session only
72
+ /advisor model reset drop the session pin and re-inherit the global defaults
68
73
  ```
69
74
 
70
75
  `/advisor on|off|toggle` are session-scoped and ephemeral: they flip a per-session override, never the persisted config. Enabling a session whose config lacks `provider`/`model` starts no model call — `/advisor status` (and the `/advisor on` reply) shows the gate reason: the advisor runs only when enabled **with** both configured. `/advisor on` is also the manual recovery path: a session advisor paused by a quota/rate-limit (`quota_exhausted` — no auto-resume timer) resumes in place, and a halted advisor (permanent model error, e.g. invalid credentials) is rebuilt fresh for the session.
71
76
 
72
- In a **dsh-tui** profile, `/advisor config` additionally reads back the composed configuration — read-only, with edit hints naming the real write paths: the TUI `/settings` screen (Advisor section, dsh-tui ≥ v0.8.0), the profile patch layer, and the shared `$DSH_HOME/settings.yaml` `advisor:` section. The `/advisor` / `on|off|status|config` commands are listed in the TUI `/` menu with subcommand completion (command discovery requires the `dsh-tui-command-trees` row — the shipped dsh-tui bundle has it).
77
+ `/advisor model set` pins a reviewer model for the **invoking session only** — an in-memory, atomic `provider + model` pair that lives for the live session (cleared on dispose, owner teardown, cold resume, or restart; a forked/new session inherits the global defaults). It rides above the persisted global defaults without rewriting them: a complete session pair is used even when the global config has no pair yet, a malformed global config still blocks every session, half-pairs are never merged, and setting/resetting never touches the enable switch. Validation resolves the pair through the LLM service before commit (60 s bound, cancellable, no auto-retry); on failure the previous selection stays untouched. `/advisor config` remains the readback of the **global defaults**, not the session state.
78
+
79
+ In a **dsh-tui** profile, `/advisor config` additionally reads back the composed configuration — the global defaults, read-only, with edit hints naming the real write paths: the TUI `/settings` screen (Advisor section, dsh-tui ≥ v0.8.0) and the profile patch layer. The `/advisor` / `on|off|status|config|model` commands are listed in the TUI `/` menu with subcommand completion (command discovery requires the `dsh-tui-command-trees` row — the shipped dsh-tui bundle has it).
80
+
81
+ On the **web**, the same session model controls ride the session header's **Advisor action** (requires a dsh web build whose shell declares the `conversation.session.header.actions` slot — dsh ≥ 0.1.7-rc.1). The action is bound to the session it sits on: it shows the effective reviewer pair, its source (`session override` / `global default`), and the live-session lifetime; **Pin this model** pins a provider + model for that session, and **Use global default** drops the pin (the reset path). It writes ONLY through the plugin's session endpoints (`/api/advisor/getSession` + `/api/advisor/setSessionModel`) — the same controller, validation, and fencing as `/advisor model`, never the persisted config — and it never falls back to the global config write when the session surface is unavailable. The control refreshes when you open it (plus on reconnect/focus while open); there is no background polling. The global card on the Plugins page stays global-only.
73
82
 
74
83
  ## Features
75
84
 
76
- - **Independent reviewer per session**: a separate model call observes the primary transcript and reviews each stepped primary turn; advisor messages are excluded from later deltas, so the advisor does not read its own advice back. One exception, accepted and bounded: notes persisted before the source-kind migration (legacy custom `kind: 'advisor'`) are no longer matched by the self-review exclusion and re-enter the delta once per full replay (e.g. after compaction) — no data loss, advisor-side self-review pollution only. This cost is accepted by design, not deferred: no legacy read arm is added deliberately, because a compatibility layer is forbidden by the project's no-backward-compatibility invariant.
85
+ - **Independent reviewer per session**: a separate model call observes the primary transcript and reviews each stepped primary turn; advisor messages are excluded from later deltas, so the advisor does not read its own advice back. The exclusion recognizes the advisor's current producer kind (`advisor`) AND both historical shapes a still-openable log can carry: the prehistoric direct `{ kind: 'advisor' }` note and the 0.1.6-era note as the V3→V4 in-memory migration rewrote it (`kind: 'plugin:advisor'`) — no generation of persisted notes is orphaned by the identity migration.
77
86
  - **Severity-ranked advice with inject/steer semantics**: at most one note per review — **nit** (a minor style, clarity, or quality suggestion; delivered via non-waking `agent.inject`, consumed at the next pre-step boundary), **concern** (a material risk or clearly better direction to weigh before continuing; delivered via waking `agent.steer`, subject to the `immuneTurns` cooldown), **blocker** (continuing clearly wastes work — contradicts an explicit user instruction, going in circles, fundamentally unsound; delivered via `agent.steer`). Delivered messages carry the `[advisor:{severity}]` prefix and are self-described advisory content:
78
87
 
79
88
  ```
80
89
  [advisor:concern] extract the helper into a module and unit-test it
81
90
  ```
82
91
 
83
- - **Explicit model gate**: `enabled` defaults to off; `enabled: true` without `provider` + `model` never starts a model call — status reports disabled-with-reason. Unknown config keys are rejected.
92
+ - **Explicit model gate**: `enabled` defaults to off; `enabled: true` without `provider` + `model` never starts a model call — status reports disabled-with-reason. The gate applies to the *effective* route after session resolution: a complete per-session override pair satisfies it for that session; a malformed global config cannot be bypassed. Unknown config keys are rejected.
84
93
  - **Zero-tool minimal start**: the reviewer is an independent model call only — no advisor tools, nothing it can do to the session besides advisory messages.
85
94
  - **No-stall failure policy**: a failing or quota-limited advisor only drops its own bounded backlog — it can never park or pollute the primary loop.
86
- - **Session-scoped controls**: `/advisor on|off|status|config` work per session; the toggles are ephemeral overrides, never persisted config.
95
+ - **Session-scoped controls**: `/advisor on|off|status|config|model` work per session; the toggles and the per-session model pin are ephemeral overrides, never persisted config — `/advisor config` always reports the global defaults. On the web, the session header's **Advisor action** drives the same per-session pin through dedicated session endpoints (see [Verify](#verify)).
87
96
 
88
97
  ![Advisor note injected into the session stream](docs/screenshots/advisor-injected-note.webp)
89
98
 
90
99
  ## Mount-only (no dsh modification)
91
100
 
92
- The plugin installs as a **pure mount**: bundle insert + client card (web Settings 插件配置) + its own gateway channel (`/api/advisor/get|set`, claimed by the host's typertGateway — the same mechanism the dsh `goals` service uses, not gated by the settings exposure allowlist) + the `/advisor` commands — no dsh patches, no postinstall step, and dsh upgrades never require re-patching.
101
+ The plugin installs as a **pure mount**: bundle insert + client card (the web Plugins page) + its own gateway channel (`/api/advisor/get|set` for the global config plus `/api/advisor/getSession|setSessionModel` for the per-session model surface, claimed by the host's typertGateway — the same mechanism the dsh `goals` service uses, not gated by the settings exposure allowlist) + the `/advisor` commands — no dsh patches, no postinstall step, and dsh upgrades never require re-patching.
93
102
 
94
103
  ## Limitations & roadmap
95
104
 
@@ -111,7 +120,7 @@ The MVP deliberately drops full omp parity. Accepted gaps (tracked in the harnes
111
120
  | Doc | Content |
112
121
  |---|---|
113
122
  | [docs/install.md](docs/install.md) | profile install (web + dsh-tui) / registry / git / tarball / local-directory variants / web Settings exposure / uninstall / `--dump-config` verification |
114
- | [docs/configuration.md](docs/configuration.md) | full `advisor` namespace reference: keys & defaults, explicit model gate (S4), settings surfaces (web card / patch layer / global settings.yaml), example YAML, live re-apply behavior |
123
+ | [docs/configuration.md](docs/configuration.md) | full advisor config reference: keys & defaults, explicit model gate (S4), config surfaces (web card / TUI `/settings` / patch layer), example YAML, live re-apply behavior |
115
124
  | [docs/consumer-api.md](docs/consumer-api.md) | developer consumption contract: package-root library API, `dsh-advisor/client` entry, `/advisor` command surface, export inventory, lifecycle |
116
125
  | [docs/verification.md](docs/verification.md) | verification records: test matrix (16 files / 319 cases), typecheck/build, CI contract, real-environment steps |
117
126
  | [docs/release.md](docs/release.md) | release process: PR-driven Release prep + Release workflows, OIDC trusted publishing, version strategy, rollback |
package/README.zh.md CHANGED
@@ -5,7 +5,7 @@
5
5
  [![npm](https://img.shields.io/npm/dt/dsh-advisor)](https://www.npmjs.com/package/dsh-advisor)
6
6
  [![license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
7
  ![node](https://img.shields.io/badge/node-%5E22.19%20%7C%7C%20%3E%3D24-339933.svg)
8
- ![dsh](https://img.shields.io/badge/dsh-0.1.5--rc.1-4B32C3.svg)
8
+ ![dsh](https://img.shields.io/badge/dsh-0.1.7--rc.1-4B32C3.svg)
9
9
  ![dsh tui](https://img.shields.io/badge/dsh%20tui-compatible-4B32C3.svg)
10
10
  [![dshfind](https://dshfind.com/api/badge/omdsh-dev/dsh-advisor?lang=zh)](https://dshfind.com/zh/plugins/omdsh-dev/dsh-advisor?ref=badge)
11
11
 
@@ -13,7 +13,7 @@
13
13
 
14
14
  **仅作建议。** advisor 从不批准或否决主 agent 的动作,也绝不会像主 agent 那样发出命令。每条送达的消息都是自我描述的 advisory 内容;一个行为异常的评审者会被端到端约束(emission guard、immuneTurns 冷却、failure policy),因此它永远不会卡住或污染主循环。
15
15
 
16
- 两个 dsh 前端均可用:**web** profile(设置 → 插件配置 → Advisor 卡片)与 **dsh-tui** 终端 profile(`/advisor` + `/advisor config`)。
16
+ 两个 dsh 前端均可用:**web** profile(侧边栏 → 插件 → dsh-advisor → Advisor 卡片)与 **dsh-tui** 终端 profile(`/advisor` + `/advisor config`)。
17
17
 
18
18
  ## 快速开始
19
19
 
@@ -28,29 +28,31 @@ dsh plugin --profile dsh-tui add dsh-advisor # dsh-tui 终端 profile
28
28
 
29
29
  ### 配置
30
30
 
31
- 在全局 dsh 设置文档(默认 `$DSH_HOME/settings.yaml`——跨 profile 共享;web Settings 卡片也写入这个文件)中添加 `advisor:` 分节:
31
+ 编辑 profile 补丁层(`~/.dsh/profiles/<profile>/cordis.patch.yml`)里 `advisor` 行的 `config`。六个字段全部是 schema-volatile 的 live 字段(dsh ≥ 0.1.7-rc.1):web 卡片与 TUI `/settings` 屏幕写入的就是这同一份 entry config——持久化在 profile 补丁层,无需重挂载即生效。(pre-0.1.7 的 `$DSH_HOME/settings.yaml` `advisor:` 分节已不存在:dsh 会在首次启动时把它导入活跃 profile 一次,并将该文件改名为 `.imported`。)
32
32
 
33
33
  ```yaml
34
- advisor:
35
- enabled: true # 总开关(默认 false)——需显式打开后生效
36
- provider: deepseek-official # enabled: true 时必填(非空)
37
- model: deepseek-flash # enabled: true 时必填(非空);网关未开放 V41 路由时回退 deepseek-v4-flash(或其它 V4 id)
38
- systemPrompt: "" # 可选;"" = 内置评审 prompt
39
- immuneTurns: 3 # 整数 ≥ 0,默认 3 —— 打断性送达后的冷却步数
40
- maxDeltaMessages: 60 # 整数 ≥ 0,默认 60 —— delta 窗口;0 = 无上限
34
+ # ~/.dsh/profiles/<profile>/cordis.patch.yml —— advisor 行的 config
35
+ - id: advisor
36
+ config:
37
+ enabled: true # 总开关(默认 false)——需显式打开后生效
38
+ provider: deepseek-official # enabled: true 时必填(非空)
39
+ model: deepseek-flash # enabled: true 时必填(非空);网关未开放 V41 路由时回退 deepseek-v4-flash(或其它 V4 id)
40
+ systemPrompt: "" # 可选;"" = 内置评审 prompt
41
+ immuneTurns: 3 # 整数 ≥ 0,默认 3 —— 打断性送达后的冷却步数
42
+ maxDeltaMessages: 60 # 整数 ≥ 0,默认 60 —— delta 窗口;0 = 无上限
41
43
  ```
42
44
 
43
45
  advisor 默认关闭。启用后,`provider` 与 `model` 为**必填**:`enabled: true` 而缺少两者之一是一个硬门禁——advisor 不会发起任何模型调用,并报告带原因的禁用状态(disabled-with-reason);未知配置键会被拒绝。
44
46
 
45
- 同一组键在**三个配置面**之间合成(后一层覆盖前一层;各处使用同一组键与同一个硬门禁,宿主侧门禁始终是所有路径上的最后防线):
47
+ 同一组键可在**三个配置面**读取与编辑(只有一份存储——上面的 advisor entry config;各处使用同一组键与同一个硬门禁,宿主侧门禁始终是所有路径上的最后防线):
46
48
 
47
- 1. **插件行 config** —— profile 补丁层(`$DSH_HOME/profiles/<profile>/cordis.patch.yml`)。这是合成 base。
48
- 2. **dsh web Settings 页 —— "插件配置"页** —— Advisor **卡片**(namespace key `advisor`),含 enabled 开关、只列出系统内已配置 provider 及其模型的 provider/model 选择框与可选字段。保存写入 `advisor` settings namespace,新会话立即生效,无需重启。卡片要求当前版本的 dsh web 构建(其 web shell 声明了 `settings.plugin.item` 卡片 slot 并能加载 `dsh.client` 声明包);它通过官方 `GatewayService` RPC 通道读写该命名空间(`/api/advisor/get` + `/api/advisor/set`),不受 settings 暴露白名单门控。卡片还会在 enabled 且必填字段为空时阻止保存。
49
- 3. **`/advisor` 指令** —— 按会话且临时:翻转的是会话级 override,从不修改持久化配置(见[验证](#验证))。
49
+ 1. **插件行 config** —— profile 补丁层(`~/.dsh/profiles/<profile>/cordis.patch.yml`)。配置就存放在这里。
50
+ 2. **dsh web 的「插件」页 —— dsh-advisor 组合包自己的页面** —— Advisor **卡片**(bundle key `dsh-advisor`),含 enabled 开关、只列出系统内已配置 provider 及其模型的 provider/model 选择框与可选字段。保存写入 advisor entry 的 config(经 config editor 落入 profile 补丁层),运行中的会话立即生效,无需重启。卡片要求 dsh web 构建的 shell 声明了 `plugins.bundle.config` 卡片 slot(dsh ≥ 0.1.7-rc.1)并能加载 `dsh.client` 声明包;它通过官方 `GatewayService` RPC 通道读写该配置(`/api/advisor/get` + `/api/advisor/set`),不受 settings 暴露白名单门控。卡片还会在 enabled 且必填字段为空时阻止保存。
51
+ 3. **`/advisor` 指令** —— 按会话且临时:翻转的是会话级 override、并为会话钉住评审模型,从不修改持久化配置(见[验证](#验证))。
50
52
 
51
- 在 **dsh-tui** profile 中,同样的五个键可在 TUI `/settings` 屏幕编辑:运行 `dsh --profile dsh-tui`、打开 `/settings`,编辑 **Advisor** 分节(`enabled` / `provider` / `model` / `immuneTurns` / `maxDeltaMessages`,每项均带中英文标签与提示)。编辑先暂存,保存时经 revision 栅栏保护的 `settings.mutate` 写入 web 卡片所写的同一个 `advisor` 命名空间 user layer,并 live 重应用、无需重启。`systemPrompt` **不是** TUI 字段(TUI text 控件为单行;多行 prompt 会被截断)——请经 web 卡片或 `$DSH_HOME/settings.yaml` 编辑。该分节要求 dsh-tui ≥ v0.8.0(随 v0.8.0+ 组合包的 `dsh-tui-settings-sections` 行提供);旧版 dsh-tui 会干净地 no-op,仍以两个文件路径——profile 补丁层 + 全局 `$DSH_HOME/settings.yaml`——作为编辑路径。`/advisor config` 仍是只读回读,seam 挂载时其编辑提示指向 `/settings` 屏幕。保存行为与 web 卡片不同:TUI seam 没有跨字段校验,一次保存可能把 `enabled: true` 与空 `provider`/`model` 一起写入——显式模型门禁会在运行时把它解析为 disabled-with-reason(可见于 `/advisor status` 与 `/advisor config`);web 卡片则会直接阻止这样的保存。完整参考 → [docs/configuration.md](docs/configuration.md)。
53
+ 在 **dsh-tui** profile 中,同样的五个键可在 TUI `/settings` 屏幕编辑:运行 `dsh --profile dsh-tui`、打开 `/settings`,编辑 **Advisor** 分节(`enabled` / `provider` / `model` / `immuneTurns` / `maxDeltaMessages`,每项均带中英文标签与提示)。编辑先暂存,保存时经 revision 栅栏保护的 `settings.mutate` 写入 web 卡片所写的同一份 advisor entry config,并 live 重应用、无需重启。`systemPrompt` **不是** TUI 字段(TUI text 控件为单行;多行 prompt 会被截断)——请经 web 卡片或 profile 补丁层编辑。该分节要求 dsh-tui ≥ v0.8.0(随 v0.8.0+ 组合包的 `dsh-tui-settings-sections` 行提供);旧版 dsh-tui 会干净地 no-op,profile 补丁层仍是编辑路径。`/advisor config` 仍是只读回读,seam 挂载时其编辑提示指向 `/settings` 屏幕。保存行为与 web 卡片不同:TUI seam 没有跨字段校验,一次保存可能把 `enabled: true` 与空 `provider`/`model` 一起写入——显式模型门禁会在运行时把它解析为 disabled-with-reason(可见于 `/advisor status` 与 `/advisor config`);web 卡片则会直接阻止这样的保存。完整参考 → [docs/configuration.md](docs/configuration.md)。
52
54
 
53
- ![dsh web Settings("插件配置")页上的 Advisor 卡片](docs/screenshots/advisor-settings-card.webp)
55
+ ![dsh web「插件」页(dsh-advisor 组合包页面)上的 Advisor 卡片](docs/screenshots/advisor-settings-card.webp)
54
56
 
55
57
  ### 验证
56
58
 
@@ -65,31 +67,38 @@ dsh --profile web --dump-config # 显示带 advisor 配置行的 "# == dsh-adv
65
67
  /advisor on enable the advisor for this session
66
68
  /advisor off disable the advisor for this session
67
69
  /advisor status show state, model, runtime status, pending count, last activity
70
+ /advisor model show the effective reviewer model and its source (session override or global default)
71
+ /advisor model set <provider> <model> pin a reviewer model for this session only
72
+ /advisor model reset drop the session pin and re-inherit the global defaults
68
73
  ```
69
74
 
70
75
  `/advisor on|off|toggle` 是会话级且临时的:它们翻转的是按会话的 override,从不修改持久化配置。启用一个 config 缺少 `provider`/`model` 的会话不会发起模型调用——`/advisor status`(以及 `/advisor on` 的回复)会显示门禁原因:advisor 只有在启用**且**两者均已配置时才运行。`/advisor on` 也是手动恢复路径:被 quota/rate-limit 暂停的会话 advisor(`quota_exhausted`——无自动恢复定时器)会在原地恢复;被终止的 advisor(永久性模型错误,如凭据无效)会为该会话全新重建。
71
76
 
72
- 在 **dsh-tui** profile 中,`/advisor config` 额外回读组合配置——只读,编辑提示指向真实的写路径:TUI `/settings` 屏幕(Advisor 分节,dsh-tui ≥ v0.8.0)、profile 补丁层与共享的 `$DSH_HOME/settings.yaml` 的 `advisor:` 分节。`/advisor` / `on|off|status|config` 指令出现在 TUI 的 `/` 菜单中并带子命令补全(指令发现要求 `dsh-tui-command-trees` 行——随附的 dsh-tui 组合包自带)。
77
+ `/advisor model set` 只为**发起调用的会话**钉住一个评审模型——一个内存中的原子 `provider + model` 对,生存期为活跃会话(dispose、owner 卸载、冷恢复或重启时清除;fork 的新会话继承全局默认值)。它叠加在持久化的全局默认值之上而不改写它们:全局配置还没有 pair 时,完整的会话对即可生效;全局配置非法时所有会话照旧被阻挡;两级之间的半个 pair 永不拼接;set/reset 从不触碰启用开关。提交前会经 LLM 服务解析校验该对(60 秒上界、可取消、无自动重试);失败时先前选择保持不变。`/advisor config` 始终是**全局默认值**的回读,不是会话状态。
78
+
79
+ 在 **dsh-tui** profile 中,`/advisor config` 额外回读组合配置——即全局默认值,只读,编辑提示指向真实的写路径:TUI `/settings` 屏幕(Advisor 分节,dsh-tui ≥ v0.8.0)与 profile 补丁层。`/advisor` / `on|off|status|config|model` 指令出现在 TUI 的 `/` 菜单中并带子命令补全(指令发现要求 `dsh-tui-command-trees` 行——随附的 dsh-tui 组合包自带)。
80
+
81
+ 在 **web** 端,同样的会话级模型控制由会话头部的 **Advisor 动作**承载(要求 dsh web 构建的 shell 声明 `conversation.session.header.actions` 插槽——dsh ≥ 0.1.7-rc.1)。该动作绑定在它所在的会话上:显示生效的评审 pair、其来源(`session override` / `global default`)与 live-session 生存期;**Pin this model** 为该会话钉住 provider + model,**Use global default** 去除钉住(即 reset 路径)。它只通过插件会话端点(`/api/advisor/getSession` + `/api/advisor/setSessionModel`)写入——与 `/advisor model` 同一控制器、同一校验与栅栏,从不写持久化配置——并且当会话控制面不可用时,绝不回退到全局配置写通道。控件在打开时刷新(打开期间断连/聚焦也会刷新);没有后台轮询。插件页上的全局卡片保持仅全局。
73
82
 
74
83
  ## 能力一览
75
84
 
76
- - **每个会话一个独立评审者**:独立的模型调用观察主 transcript 并评审每个 stepped 主 turn;advisor 消息被排除在此后的 delta 之外,因此 advisor 不会读回自己的建议。一个例外,已接受且有界:迁移前写入、带旧自定义 kind(`kind: 'advisor'`)的 note 已不再被自审排除匹配,每次全量重放(如 compaction 之后)都会重新进入 delta 一次——无数据丢失,仅 advisor 侧自审污染。这一代价是有意接受的,而非被推迟:不刻意添加旧格式读取分支,因为兼容层被项目的「不保持向后兼容」不变量禁止。
85
+ - **每个会话一个独立评审者**:独立的模型调用观察主 transcript 并评审每个 stepped 主 turn;advisor 消息被排除在此后的 delta 之外,因此 advisor 不会读回自己的建议。自审排除同时识别 advisor 当前的 producer kind(`advisor`)与仍可打开日志中可能出现的两种历史形状:史前直接 `{ kind: 'advisor' }` 的 note,以及 0.1.6 时代的 note 经 V3→V4 内存迁移后的形状(`kind: 'plugin:advisor'`)——身份迁移不会孤儿化任何一代已持久化的 note。
77
86
  - **按严重度排序的建议 + inject/steer 语义**:每次评审至多发出一条 note——**nit**(轻微的样式、清晰度或质量建议;经非唤醒的 `agent.inject` 送达,在下一个 pre-step 边界消费)、**concern**(继续之前值得权衡的重大风险或明显更优的方向;经唤醒的 `agent.steer` 送达,受 `immuneTurns` 冷却约束)、**blocker**(继续下去明显是在浪费工作——与显式用户指令矛盾、原地打转、根本性不可行;经 `agent.steer` 送达)。送达的消息携带 `[advisor:{severity}]` 前缀且为自我描述的 advisory 内容:
78
87
 
79
88
  ```
80
89
  [advisor:concern] extract the helper into a module and unit-test it
81
90
  ```
82
91
 
83
- - **显式模型门禁**:`enabled` 默认关闭;`enabled: true` 而缺少 `provider` + `model` 时绝不发起模型调用——状态报告 disabled-with-reason。未知配置键会被拒绝。
92
+ - **显式模型门禁**:`enabled` 默认关闭;`enabled: true` 而缺少 `provider` + `model` 时绝不发起模型调用——状态报告 disabled-with-reason。门禁在会话解析**之后**作用于*有效*路由:完整的会话级覆盖对可为其会话满足门禁;非法的全局配置不可被绕过。未知配置键会被拒绝。
84
93
  - **零工具的最小启动**:评审者只是一个独立的模型调用——无 advisor tools,除了 advisory 消息之外它无法对会话做任何事。
85
94
  - **不卡主循环的失败策略**:失败或 quota 耗尽的 advisor 只会丢弃自己有界的 backlog——永远不会卡住或污染主循环。
86
- - **会话级控制**:`/advisor on|off|status|config` 按会话工作;开关是临时的 override,从不修改持久化配置。
95
+ - **会话级控制**:`/advisor on|off|status|config|model` 按会话工作;开关与会话级模型钉住都是临时的 override,从不修改持久化配置——`/advisor config` 始终报告全局默认值。在 web 端,会话头部的 **Advisor 动作**经由专属会话端点驱动同一个会话级钉住(见 [Verify](#verify))。
87
96
 
88
97
  ![注入到会话流中的 advisor 建议](docs/screenshots/advisor-injected-note.webp)
89
98
 
90
99
  ## 纯挂载(零 dsh 修改)
91
100
 
92
- 插件以**纯挂载**方式安装:bundle 插入 + 客户端卡片(web Settings "插件配置")+ 自有 gateway 通道(`/api/advisor/get|set`,由宿主 typertGateway 认领——与 dsh 内建 `goals` 服务同一机制,不受 settings 暴露白名单门控)+ `/advisor` 指令——无 dsh 补丁、无 postinstall 步骤,dsh 升级永不需重打。
101
+ 插件以**纯挂载**方式安装:bundle 插入 + 客户端卡片(web「插件」页)+ 自有 gateway 通道(`/api/advisor/get|set` 承载全局配置,`/api/advisor/getSession|setSessionModel` 承载会话级模型面,由宿主 typertGateway 认领——与 dsh 内建 `goals` 服务同一机制,不受 settings 暴露白名单门控)+ `/advisor` 指令——无 dsh 补丁、无 postinstall 步骤,dsh 升级永不需重打。
93
102
 
94
103
  ## 限制与路线图
95
104
 
@@ -111,7 +120,7 @@ MVP 有意放弃与 omp 的完整对等。已接受的差距(在 harness 迭
111
120
  | 文档 | 内容 |
112
121
  |---|---|
113
122
  | [docs/install.zh.md](docs/install.zh.md) | profile 安装(web + dsh-tui)/ registry / git / tarball / 本地目录变体 / web Settings 暴露 / 卸载 / `--dump-config` 验证 |
114
- | [docs/configuration.md](docs/configuration.md) | `advisor` 命名空间全字段:键与默认值、显式模型门禁(S4)、配置面(web 卡片 / 补丁层 / 全局 settings.yaml)、示例 YAML、live 重应用行为 |
123
+ | [docs/configuration.md](docs/configuration.md) | advisor 配置全字段:键与默认值、显式模型门禁(S4)、配置面(web 卡片 / TUI `/settings` / 补丁层)、示例 YAML、live 重应用行为 |
115
124
  | [docs/consumer-api.md](docs/consumer-api.md) | 开发者消费契约:包根库 API、`dsh-advisor/client` 入口、`/advisor` 指令面、导出清单、生命周期 |
116
125
  | [docs/verification.md](docs/verification.md) | 验证记录:测试矩阵(16 文件 / 319 用例)、typecheck/build、CI 契约、真实环境步骤 |
117
126
  | [docs/release.md](docs/release.md) | 发布流程:PR 驱动的 Release prep + Release 工作流、OIDC trusted publishing、版本策略、回滚 |
@@ -614,7 +614,7 @@ export class AdvisorRuntime {
614
614
  // so the budget goes to the JSON frame, and raise it for headroom
615
615
  // (ADVISOR_MAX_TOKENS). The advisor's job is a short structured note —
616
616
  // reasoning is not needed. The same applies to any reasoning-capable
617
- // model, including the 0.1.5-rc.1 default deepseek-flash. Capability-gated
617
+ // model, including the 0.1.5-rc.2 default deepseek-flash. Capability-gated
618
618
  // (qc2 W-1 / qc1 W-1 / qc3 F-3): `resolveReasoningEffort` passes the
619
619
  // branded 'off' ONLY when the resolved model declares it; otherwise the
620
620
  // option is omitted entirely (the dsh LlmRuntime would reject an explicit
@@ -1,15 +1,17 @@
1
1
  /**
2
2
  * Advisor settings card (plan dsh-advisor-plugin-config-card-ux, task 1): the
3
- * card registered into the "插件配置" settings page's `settings.plugin.item`
4
- * keyed slot (key `advisor` — the settings namespace the card edits). It keeps
3
+ * card registered into the Plugins page's `plugins.bundle.config` keyed slot
4
+ * (key `dsh-advisor` — the bundle's package name the page dispatches). It keeps
5
5
  * the n5 gateway channel — the store
6
6
  * reads/writes the advisor config through `/api/advisor/get` +
7
7
  * `/api/advisor/set` (KD-G3) — while the card chrome is rebuilt to replicate
8
8
  * the upstream `PluginCard` contract (self-drawn: the upstream client value
9
9
  * face exports no reusable card). The chrome: a collapsible box whose header
10
10
  * is a button stacking the plugin name over its description, with a dirty
11
- * "unsaved" pill and a rotating chevron (`IconChevronDownOutline14` from
12
- * ui-primitives), `aria-expanded`/`aria-label` like the upstream header; a
11
+ * "unsaved" pill and a rotating chevron (`IconChevronDownOutlineRegular` from
12
+ * ui-primitives — 0.1.7-rc.1 moved the rendered size out of the icon name
13
+ * into the `size` prop; the chevron's drawn size is still 14),
14
+ * `aria-expanded`/`aria-label` like the upstream header; a
13
15
  * divider under the header; then the form content; then a footer with the
14
16
  * failed message + Discard/Save carrying the upstream disabled semantics —
15
17
  * save = `!dirty || invalid || saving`, discard = `!dirty || saving` (KD-U1,
@@ -77,18 +79,20 @@ export interface AdvisorCardInjected {
77
79
  };
78
80
  }
79
81
  /**
80
- * Props the renderer binds for the card: the `settings.plugin.item` runtime
81
- * share (empty owner props), the framework-synthesized `t` seat for the
82
- * declared `settings.advisor` namespace (KD-1 — `t` is NOT part of the inject
83
- * face), and the registrant's business face.
82
+ * Props the renderer binds for the card: the `plugins.bundle.config` runtime
83
+ * share (the owner passes the `view` the page asks for — this seat is
84
+ * `page`-only, so the self-chromed card needs no branch for it), the
85
+ * framework-synthesized `t` seat for the declared `settings.advisor` namespace
86
+ * (KD-1 — `t` is NOT part of the inject face), and the registrant's business
87
+ * face.
84
88
  */
85
- export type AdvisorCardProps = PropsRuntime<'settings.plugin.item'> & PropsLocale<'settings.advisor'> & InjectFace<AdvisorCardInjected>;
89
+ export type AdvisorCardProps = PropsRuntime<'plugins.bundle.config'> & PropsLocale<'settings.advisor'> & InjectFace<AdvisorCardInjected>;
86
90
  /**
87
- * Render the advisor card inside the plugin-config section, replicating the
88
- * upstream PluginCard chrome (KD-U1): a collapsible `<li>` with a header
89
- * button (name over description, dirty pill, rotating chevron, aria) and,
90
- * when open, a divided body holding the readOnly notice, the form, and the
91
- * footer (failed message + Discard/Save).
91
+ * Render the advisor card inside its bundle's configuration section on the
92
+ * Plugins page, replicating the upstream PluginCard chrome (KD-U1): a
93
+ * collapsible block with a header button (name over description, dirty pill,
94
+ * rotating chevron, aria) and, when open, a divided body holding the readOnly
95
+ * notice, the form, and the footer (failed message + Discard/Save).
92
96
  * @param props - slot-delivered injected dependencies and the synthesized t seat.
93
97
  * @returns the card.
94
98
  */
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Advisor session header action (B2 — issue #88 web session control): the
3
+ * entry registered into the shell-declared `conversation.session.header.actions`
4
+ * list slot (title-adjacent Session actions — NOT the singleton
5
+ * primary-model slot, NOT the root plugin card; the global card stays
6
+ * global-only). The slot is session-scoped, so the framework resolves the
7
+ * bound SessionId into the inject factory (src/client/index.ts), which builds
8
+ * ONE {@link AdvisorSessionModelController} per session scope binding — the
9
+ * renderer memoizes that face per (entry × session binding), so the instance
10
+ * lives and dies with the binding and every fetch/write carries its own
11
+ * sessionId (a late response for an old binding can never render another
12
+ * session's state).
13
+ *
14
+ * The control shows the authoritative session snapshot — the effective
15
+ * reviewer pair, its source (`session`/`global`), the live-session lifetime,
16
+ * the effective switch, and the S4 gate reason when blocked — and supports
17
+ * set (provider + model, staged through the shared provider directory) and
18
+ * **Use global default** (reset). It rides ONLY the plugin session endpoints
19
+ * (`/api/advisor/getSession` + `/api/advisor/setSessionModel`); when the
20
+ * surface is unavailable the menu renders a notice and offers NO writes — it
21
+ * never falls back to the global `advisor/set` channel.
22
+ *
23
+ * Fetch discipline: fetch on menu open, on binding change (a new binding gets
24
+ * a fresh controller whose first open fetches), and on refresh signals while
25
+ * open (connection reset / window focus — the epoch store bumps, the watcher
26
+ * refetches). Every response settles through the controller's request fence,
27
+ * so a superseded (stale) response is discarded wholesale. NO push-event
28
+ * contract, NO polling: an open menu is a refreshable snapshot, and the
29
+ * closed trigger is NEUTRAL — the plugin name only, never a model label
30
+ * falsely claiming continuous synchronization.
31
+ */
32
+ import { type ReactNode } from 'react';
33
+ import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
34
+ import type { SnapshotStore } from '@deepseek-ai/dsh-client-store';
35
+ import type { AdvisorSessionMenuState, AdvisorSessionModelController, AdvisorSettingsState, AdvisorSettingsStore } from './advisor-store.ts';
36
+ /**
37
+ * Injected dependencies of {@link AdvisorSessionAction} (slot `inject`,
38
+ * built per session binding by the registration's inject factory). The
39
+ * `hooks` compartment carries the bare snapshot sources; the renderer binds
40
+ * each as a `use<Name>` selector hook the component consumes.
41
+ */
42
+ export interface AdvisorSessionActionInjected {
43
+ /** The session this occurrence is bound to (framework-resolved). */
44
+ readonly sessionId: string;
45
+ /** The per-session session-model controller (one per binding). */
46
+ readonly controller: AdvisorSessionModelController;
47
+ /** The shared provider/model directory (READ-only reuse of the card's store). */
48
+ readonly directory: AdvisorSettingsStore;
49
+ /** Bare snapshot sources the renderer binds as selector hooks. */
50
+ readonly hooks: {
51
+ /** The per-session menu snapshot. */
52
+ readonly snapshot: SnapshotStore<AdvisorSessionMenuState>;
53
+ /** Refresh-signal epoch (bumped on connection reset / window focus). */
54
+ readonly refreshSignal: SnapshotStore<{
55
+ epoch: number;
56
+ }>;
57
+ /** The shared provider/model directory state. */
58
+ readonly directory: SnapshotStore<AdvisorSettingsState>;
59
+ };
60
+ }
61
+ /**
62
+ * Props the renderer binds for the action: the session-scoped runtime share
63
+ * (the slot's owner is a marker — no owner-specific values), the
64
+ * framework-synthesized `t` seat for the declared `settings.advisor`
65
+ * namespace, and the registrant's per-binding business face.
66
+ */
67
+ export type AdvisorSessionActionProps = PropsRuntime<'conversation.session.header.actions'> & PropsLocale<'settings.advisor'> & InjectFace<AdvisorSessionActionInjected>;
68
+ /**
69
+ * Render the session header's Advisor action: a neutral trigger plus, when
70
+ * open, a popover panel holding the refreshable snapshot and the pin/reset
71
+ * controls.
72
+ * @param props - slot-delivered injected dependencies and the synthesized t seat.
73
+ * @returns the action.
74
+ */
75
+ export declare function AdvisorSessionAction(props: AdvisorSessionActionProps): ReactNode;
@@ -347,3 +347,134 @@ export declare class AdvisorSettingsStore {
347
347
  * @param controller - the card store.
348
348
  */
349
349
  export declare function refreshIfLoaded(controller: AdvisorSettingsStore): void;
350
+ /**
351
+ * The wire `snapshot` value of `/api/advisor/getSession` and of a successful
352
+ * `/api/advisor/setSessionModel` — client mirror of the node-side
353
+ * `AdvisorSessionSnapshotWire` (src/gateway.ts). Kept as a local structural
354
+ * type because the client bundle may not value-import the node-side module;
355
+ * the wire normalization omits absent keys (modelOverride / modelSource /
356
+ * effectiveModel / disabledReason are simply missing from the JSON).
357
+ */
358
+ export interface AdvisorSessionSnapshotView {
359
+ /** The session the snapshot describes (echoes the request's binding). */
360
+ sessionId: string;
361
+ /** Effective switch for this session (session override ?? global). */
362
+ enabled: boolean;
363
+ /** Live-session lifetime marker — the snapshot is never durable. */
364
+ lifetime: 'live-session';
365
+ /** The session's pinned atomic pair; absent while it inherits. */
366
+ modelOverride?: {
367
+ provider: string;
368
+ model: string;
369
+ };
370
+ /** Where the effective route comes from; present iff a pair exists. */
371
+ modelSource?: 'session' | 'global';
372
+ /** The effective route; absent when neither level has a complete pair. */
373
+ effectiveModel?: {
374
+ provider: string;
375
+ model: string;
376
+ };
377
+ /** Present iff the S4 explicit gate blocks model calls. */
378
+ disabledReason?: string;
379
+ }
380
+ /** One `setSessionModel` selection on the wire: an atomic pair, or null (= reset). */
381
+ export type AdvisorSessionSelection = {
382
+ readonly provider: string;
383
+ readonly model: string;
384
+ } | null;
385
+ /**
386
+ * The RPC result union of the session endpoints: a snapshot, or a
387
+ * plugin-domain tagged error carried in the returned data (never a thrown
388
+ * coded failure — the dsh failure vocabulary stays frozen).
389
+ */
390
+ export interface AdvisorSessionRpcPayload {
391
+ snapshot?: AdvisorSessionSnapshotView;
392
+ error?: {
393
+ tag: string;
394
+ message: string;
395
+ };
396
+ }
397
+ /** The session header action's menu snapshot. */
398
+ export interface AdvisorSessionMenuState {
399
+ /** idle → loading (first fetch) → ready; refreshes keep `snapshot` visible. */
400
+ phase: 'idle' | 'loading' | 'ready';
401
+ /** The authoritative snapshot; null until the first successful load. */
402
+ snapshot: AdvisorSessionSnapshotView | null;
403
+ /** Last failure text for display (null when healthy). */
404
+ error: string | null;
405
+ /**
406
+ * Latch: the session control surface is unavailable (no elected owner on
407
+ * the host, the target session is unknown/disposed, or the channel failed).
408
+ * An unavailable menu offers NO writes — it never falls back to the global
409
+ * `advisor/set` channel. Recovered by a later successful refresh.
410
+ */
411
+ unavailable: boolean;
412
+ /** A set/reset write is in flight. */
413
+ pending: boolean;
414
+ }
415
+ /**
416
+ * Per-session controller for the session header's Advisor action (one
417
+ * instance per session — the slot renderer memoizes the inject face per
418
+ * entry × session scope binding, so the instance lives and dies with the
419
+ * binding). Every call sends ITS OWN `sessionId` and commits only into its
420
+ * own store: a late response for an old binding can never mutate another
421
+ * session's state (the structural binding fence). Ordering within one session
422
+ * is fenced by a monotonic `requestSeq` — a newer open/refresh/write
423
+ * supersedes an unresolved older op, whose settle is discarded wholesale
424
+ * (the same generation-fence shape the host controller uses).
425
+ */
426
+ export declare class AdvisorSessionModelController {
427
+ private readonly rpc;
428
+ readonly sessionId: string;
429
+ /** The snapshot the menu renders from (uSES-safe store). */
430
+ readonly store: SnapshotStore<AdvisorSessionMenuState>;
431
+ /** Fence token: a newer op makes every older settle stale. */
432
+ private requestSeq;
433
+ /** Last consumed refresh-signal epoch (reconnect / focus bumps). */
434
+ private lastSignal;
435
+ constructor(sessionId: string, rpc: ClientConnectionRpc);
436
+ /**
437
+ * Record the refresh-signal epoch and report whether it is NEW. The open
438
+ * handler always refetches and merely marks the signal current; the render
439
+ * watcher calls this while open and refetches only on a true return (a
440
+ * reconnect/focus bump). Idempotent per epoch, so a re-render never
441
+ * double-fetches.
442
+ */
443
+ takeSignal(epoch: number): boolean;
444
+ /**
445
+ * Fetch the authoritative snapshot (`/api/advisor/getSession`). Called on
446
+ * menu open and on every refresh signal while open. A stale settle (a newer
447
+ * op superseded this fetch) touches nothing. An `advisor/unavailable` /
448
+ * `advisor/session-unknown` tag or a transport failure latches
449
+ * `unavailable` (writes stay off until a later successful refresh); other
450
+ * tagged outcomes leave the surface usable with the error shown.
451
+ */
452
+ refresh(): Promise<void>;
453
+ /**
454
+ * Pin the atomic `{provider, model}` pair for THIS session
455
+ * (`/api/advisor/setSessionModel`, selection = the pair). Refuses outright
456
+ * while `unavailable` (store-side defense-in-depth — an unavailable menu
457
+ * must never issue a write, and must never fall back to the global
458
+ * `advisor/set` channel). The host validates the pair through the same
459
+ * rules as the `/advisor model set` command face.
460
+ */
461
+ setSessionModel(provider: string, model: string): Promise<void>;
462
+ /**
463
+ * Drop the session pin and re-inherit the CURRENT global defaults
464
+ * (`selection: null`). Never touches the enable override; reset to a
465
+ * missing global pair still succeeds — the returned snapshot reports the
466
+ * gate-blocked/no-call state, which the menu renders truthfully.
467
+ */
468
+ resetSessionModel(): Promise<void>;
469
+ /** Shared write path (both arms ride the same endpoint + fence + outcome mapping). */
470
+ private write;
471
+ /**
472
+ * Commit one settled outcome. Guards (seq checked by the callers before
473
+ * reaching here): tagged `advisor/unavailable` / `advisor/session-unknown`
474
+ * and transport failures latch `unavailable` (no writes offered, last good
475
+ * snapshot kept for context); other tagged outcomes keep the surface usable
476
+ * and surface the message (the host left the previous selection untouched);
477
+ * a snapshot commits as the new authoritative state.
478
+ */
479
+ private applyOutcome;
480
+ }