dshana · diff
git:20261001.c585204 to git:20261002.6fc0013
2 added, 2 removed. Audit A to A.
---
name: dshana
description: "dshana App(把 DeepSeek Harness 接进 Hana 的受管子代理执行器)的使用、排错与工具手册。触发场景:提交/查询/取消 DSH 任务、应答审批(dshana action=open/reply/close/get/approve 任一动作前)、DSHana 卡显示未启动/启动中/需要处理(三态自举页)、DSH 起不来或启动超时、DSH 任务失败排查、默认模型怎么配、DSH Web UI 打不开、主题跟随宿主、DeepSeek Harness 相关。遇到 dshana 相关需求优先读本技能再动手。"
---
# dshana 使用、排错与工具手册
DSHana 把 DeepSeek Harness(DSH)作为**受管子代理执行器**接进 Hana:App 加载后自动拉起一个受管 Node runtime,里面跑 DSH web 服务;DSH 前端以**同文档注入**方式挂进 DSHana 卡(不是 iframe 内嵌)。DSH 依赖随 App 包物化,**运行时不需要安装任何东西**。
## 架构一句话
`apply(ctx)` 注册工具与路由 → 微任务触发 `ctx.runtime.start` 拉起受管 runtime → 壳页轮询 boot 状态 → 就绪后取 DSH index 注入当前页面。DSH 与宿主之间:指令走 runtime 控制面(`/_control` + loopback HTTP RPC),模型推理走宿主 `ctx.models`,凭据不进 DSH 进程。
## 首次安装(无需配置)
- **无需装依赖、无需配 Node**:DSH 及其依赖树随包分发在安装目录 `node_modules`,我们的子插件也落在那里(`node_modules/@dshana`);启动只做 DSH boot + 服务监听,不写数据目录里的任何东西。
- - **无需配 API Key / 模型**:推理经受管 runtime 内 `hana.models` 发起,provider 凭据留在宿主。DSH 自带的两个 LLM adapter 行(`llm-deepseek` / `llm-pi-ai`)在我们的 roster patch 里停掉,llm 路由只剩宿主目录那几条(那两行要凭据库里的 key,而凭据在宿主手里,它们只会摆出选到就报 `no API key` 的路由);设置里那页「模型」也一并停掉(它只编辑这两行的 settings 段,停掉后没可编辑对象,只剩空壳)。会话的模型跟着「谁开的」走:工具建的会话按**调用方那张角色卡配的模型**开(`agents/<id>/config.yaml` 的 `models.chat`,经 `agent:list` / `agent:config` 读,见 `app/agents.read`);App 设置页的「会话模型」可以改成「自定义模型」固定一条(`sessionModelProvider` / `sessionModelModel`,可选该模型支持的推理强度 `sessionModelReasoningEffort`;缺省是「复用调用方」);DSH 自己那格默认模型(`agent-default-model`)本页不经手:你手设过、而已不在宿主目录里时,就地对账换一条可服务的(优先留在原 provider 里换,再退角色卡模型、目录第一条;日志有「默认模型对账」,见 `packages/models/src/model-default-guard.ts`)。
+ - **无需配 API Key / 模型**:推理经受管 runtime 内 `hana.models` 发起,provider 凭据留在宿主。DSH 自带的两个 LLM adapter 行(`llm-deepseek` / `llm-pi-ai`)在我们组合层的补丁里停掉,llm 路由只剩宿主目录那几条(那两行要凭据库里的 key,而凭据在宿主手里,它们只会摆出选到就报 `no API key` 的路由);设置里那页「模型」也一并停掉(它只编辑这两行的 settings 段,停掉后没可编辑对象,只剩空壳)。会话的模型跟着「谁开的」走:工具建的会话按**调用方那张角色卡配的模型**开(`agents/<id>/config.yaml` 的 `models.chat`,经 `agent:list` / `agent:config` 读,见 `app/agents.read`);App 设置页的「会话模型」可以改成「自定义模型」固定一条(`sessionModelProvider` / `sessionModelModel`,可选该模型支持的推理强度 `sessionModelReasoningEffort`;缺省是「复用调用方」);DSH 自己那格默认模型(`agent-default-model`)本页不经手:你手设过、而已不在宿主目录里时,就地对账换一条可服务的(优先留在原 provider 里换,再退角色卡模型、目录第一条;日志有「默认模型对账」,见 `packages/models/src/model-default-guard.ts`)。
- **一张会话卡,两个挂载态**:`open` 与 `reply` 的回执都带一张 `details.card`(route 上 `act=` 区分,抬头固定 `DSHana`),落点是 `ui/stream.html`。聊天流里它只画入口行(`act` 对应的小标题 + 目录 / 会话 / 任务三行坐标),不注入 DSH、不带票据,所以不叠也不冻结;用户把这张卡**取出**(聊天卡的右上角菜单 / 拖拽,宿主手势,不经 App 代码)到黑板或拆窗后,宿主把同一 route 装到新的挂载上,页面那时才装配中继、注入完整 DSH 现场。App 不为它声明 manifest 卡、也不调 `hana.cards.open`——卡片中心不占一格,会话历史归 DSH 自己的侧边栏(打开 DSH UI 即可翻看),App 不另存清单。页怎么认挂载态:聊天流卡的 `hana.envelope` 的 height 是 `flexible`,黑板 / 拆窗是 `fixed`(见 `packages/ui/src/stream-entry.ts`)。聊天流态只引轻半(`stream.html` 的内联脚本首帧就写坐标 + `stream-entry.ts` 认态,并把这段会话的跟踪态放进小标题行,detail 走悬停提示);黑板 / 拆窗态才动态 `import()` 重型 chunk(`stream-stage.tsx`:React 三态 + DSH 注入 + 主题),那一面不带任何 App 状态行。对话底部的任务 chip 仍由宿主派生(`chipVisibility: "show"` 显式声明)。
- **会话卡带 sid**:卡 route 带 `?sid=<DSH session id>` 时钉住那一段会话;不带 `sid` 的会话卡(例如从入口行取出后又手改过 route)跟随跨面共用的当前选中。会话历史归 DSH 自己的侧边栏(打开 DSH UI 即可翻看),App 不另存清单。
- **默认模型**:DSH 自己的 `agent-default-model`(`DSH_HOME/settings.yaml`)——用户层为空时回落到 base 层那份官方路由。界面里直接开的会话在 DSH 自己的模型选择器里选一条,候选就是宿主目录那几条;App 设置页只列候选给「会话模型」用,不经手这格。
- **目录选择器**:DSH 的 workspace 选择对话框由 `directory-picker` seam 提供,官方 web-app 层挂的 `directory-picker-auto` 在 win32 + loopback 下挑 native;native 的客户端半优先读页面里的 `__DSH_DIRECTORY_PICKER__`(官方桌面壳由 preload 注入、弹 Electron 对话框),没桥才叫宿主进程的 OS chooser——后者要在宿主进程里 spawn 一个子进程跑 `IFileOpenDialog`(koffi 走 COM,还先合成一次 Alt 抢前台),上游写明它只适合「操作者坐在宿主屏幕前」,而本形态的受管 runtime 是沙箱里的后台子进程,开不出来。壳页在注入 DSH index 前把桥装上(`packages/ui/src/dsh-inject.ts` 的 `installDirectoryPickerBridge`),弹窗改由宿主出:`hana.resources.pick`,`mode=directory`。用户看到的是自己机器上的系统弹窗,选择器不经沙箱。
- **数据目录**:固定用 App 内置独立目录(App 数据目录下的 `.dsh`),开箱即用;共享已有目录 / 切换数据源暂不提供。
- `dshana(action="open")` 每次调用**必须显式传 `cwd`**;它必须是**已存在的绝对目录**(提交前校验,分两段:相对路径在 App 侧直接拒掉;「存在 / 是目录」由受管 runtime 的控制面动作 `cwd-check` 判——App 宿主半的 `node:fs` 只覆盖应用自己的目录,用它 stat 用户路径会一律失败,而那个失败与「目录不存在」分不开)。会话一旦建立,cwd 就是记录值,之后每次 spawn(命令、终端)都从它出发——别拿一次性 scratch 目录当会话根。这道存在性校验是**尽力而为**:控制面查询本身失败时只记日志并放行(不拿一次控制面抖动把合法 cwd 一起拒掉),那种情况下不合法的 cwd 会走到建会话那一步,由 provider 级的工作目录守卫在 spawn 时兜住。
## DSHana 卡三态
壳页轮询 `GET /api/apps/dshana/routes/dshana/boot-state`,按 `phase` 渲染:
| 状态 | 表现 | 怎么办 |
|---|---|---|
| 未启动(idle) | 台面只有一行「DSH 未启动」 | 打开本卡会补一次启动请求;也可以等自动链 |
| 启动中(starting) | 细圆环 + 「正在启动 DSH…」 | 等即可(首次含 DSH boot) |
| 就绪(ready) | 页面装载 DSH Web UI | 直接用 |
| 需要处理(error / stopped) | 状态行 + 一块 `<pre>`(code / message / note / runtimeId / port) | 看 `<pre>` 定位;自动链按退避重试,端口占用自动换端口 |
**读状态的出口**:`boot-state`(壳页与 Agent 都用;含 phase/error/userText)。App 侧不写文件日志——日志一律走宿主 `ctx.logger`,受管子进程输出由宿主运行日志捕获。
## 工具手册:`dshana(action, …)`
宿主 Agent 面**仅此一个工具**(一个插件一个同名工具,动作以顶层 `action` 区分),装配见 `packages/tools/src/index.ts`,各动作见 `packages/tools/src/actions/<action>.ts`,每个文件 = 一个同名操作。
语义对齐 subagent:`open` ≈ `subagent`(创建即带任务)、`reply` ≈ `subagent_reply`(按句柄续同一个)、`close` ≈ `subagent_close`(收工);`get` / `list` / `approve` 是本项目特色(subagent 没有)。
推理经受管 runtime 内 `hana.models` 发起,消耗宿主 provider 额度,provider 凭据留在宿主。
### 参数契约
顶层 `action` 必填,每个子命令只认自己的字段(`oneOf` 分支 + `additionalProperties: false`,所以 `open` 的 schema 里没有 `approvalId`)。
| action | 必填 | 可选 | 语义 |
|---|---|---|---|
| `open` | task, cwd | label, timeout, agentPreset, reasoningEffort, provider, model | 开一个 DSH 子代理并交首件活(新建会话 + 立即提交首条 prompt) |
| `reply` | task | taskId 或 sessionId(二选一), timeout, agentPreset, reasoningEffort, provider, model | 往同一个子代理续发消息 |
| `close` | 无 | taskId 或 sessionId(至少一个) | 取消正在跑的任务 |
| `get` | 无 | taskId 或 sessionId(至少一个) | 回看该会话最近一轮的最终结论 |
| `approve` | approvalId | outcome, taskId 或 sessionId | 应答挂起审批 |
> 查任务走宿主提供给 Agent 的内置任务查询工具(模型侧,本环境是 `check_pending_tasks`):dshana 的 open/reply 建的就是本会话的后台任务,本来就出现在那份清单里,不需要本工具另开一扇只读门。
**句柄与凭证**:`taskId`(open/reply 返回)与 `approvalId` 是**句柄路径**,工具自己解析会话并按宿主记录的来源会话校验归属;`sessionId`(形如 `session-<uuid>`)是**凭证路径**,显式传入即视为"我要跨对话操作",跳过归属校验。
### action=open:开一个子代理并交首件活
- **task + cwd 必填**;不允许传 `sessionId`(续会话用 `reply`)
- **固定异步**:立即返回 `{ content, details: { dsh: { action: "open", taskId, sessionId, rpcId, status: "running", delivery: "next-step", cwd } } }`(回执里只有文本与 dsh 坐标,不挂流内卡);任务在后台执行,完成/失败按回执里的 `delivery` 档投递回发起会话:结果在下一个输入点自动贴回,不必为等它结束回合(会话空闲时自动起新一轮);要看过程或最终结论用 `get`
- **投递档位(回执里的 `delivery`)**:宿主不替作者默选档位,本 App 在 create 时显式声明,档位定死后 update 改不了。`next-step`(当前值)=结果在下一个输入收集点贴回本会话,相当于 `session:send` 的 `steer`:不打断在途请求,也不要求模型结束回合专门等;`next-turn` 才是本回合结束后另起一轮(`followUp`)。回执里的值就是实际档位,别自行推断。
- **句柄**:返回值里的 `taskId` 就是后续 `reply` / `close` / `get` 用的句柄,优先用它
- `label` 是显示名(宿主任务列表与结果通知里可见),缺省按动作给默认前缀
- 提交链路:`ctx.tasks.create` → 受管 runtime 就绪 → `session.create` →(显式传 provider/model/effort 时才 `selectModel`)→ 绑定回写宿主任务记录(`ctx.tasks.update` 的 `metadata.dsh`,DSH 坐标的事实源)→ `session.prompt`(queue)→ runtime task-bridge 回投终态
### action=reply:续同一个子代理
- **task 必填**;目标二选一:`taskId`(句柄,默认推荐)或 `sessionId`(凭证,跨对话用)
- cwd 沿用会话已有值(持久非活跃会话自动 resume)
- 同会话多次 reply 由 App 侧串行化,按提交顺序排队,不并发
### action=close:取消正在跑的任务
- 目标二选一:`taskId`(句柄,默认)或 `sessionId`(凭证)
- 链路:通知 DSH `session.cancel`(中止模型流 / 工具 / 终端)→ 收敛为取消终态;只停本工作,不影响共享 runtime 上的其他会话
- **异步**:回执只说「已请求取消」,**不等确认窗口**(15s 窗口在后台走);确认或超窗升级的证据随后台任务通知(投递回发起会话)与 App 日志落定。占着工具回调等确认会堵住宿主通道,多张卡同时取消时尤甚
- **取消打在哪个时刻,结局不同**:砸在“回合还没跑起来”(刚 open / 刚 reply)上,DSH 能确认中止(也常直接走上面那条异步回执);砸在**回合已跑完**的任务上,没有可中止的在途物,宿主任务会被升级标记 canceled(正常语义:记录跟着最后一条命令走)
- **与 subagent_close 的差异**:DSH 会话是持久的、随时可 resume,没有实例槽位这回事,`close` 只取消当前活动任务,不“释放实例”
- 句柄反查不到(任务已被回收 / 宿主记录里的绑定已缺失)会**明确报错**,不会拿猜出来的会话继续操作
### action=get:回看某一轮最终结论
| | |
|---|---|
| 目标 | `taskId`(句柄)或 `sessionId`(凭证) |
| 取数 | `session/list` 定该会话读位点 `projections.asOfSeq` → `session/page` 在该 cut 上取尾部一窗 records |
| 口径 | **最后一次 user 消息之后、最后一次 assistant 输出**就是本轮结论(一次 open/reply = 一轮);文本截断 ≤4000 |
会显式标注、不静默篡改的情形:本轮尚无输出(退到更早的最近结论)/窗口内无 user 消息/该轮被中断/**该轮以错误结束**(模型或工具报错时 DSH 只写 `attempt` + `turn/end`,这里把错误原因透出来)/还有更早轮次未读。
注:会话日志是 V3 格式,**App 不自读 `session_projcache.json` / `session.jsonl.zstd`**(格式演进交回官方);DSH 未启动时 list/get 不可用。
### action=approve:应答挂起审批
- **approvalId 必填**(审批通知里带;同一任务可挂起多个审批,逐个应答)——它是唯一句柄,会话由工具解析(句柄路径会校验归属);`sessionId` 仅在"我要跨对话"时显式传
- **outcome**:`allowed-once`(默认,放行本次)/ `rejected`(拒绝)
- **决策看 args(具体要执行什么),不听 reason(模型自述不可尽信)**:合理放行,危险拒绝。审批请求的 `label` 写作“工具名 + 具体操作 + 申请的权限档”,`details` 同源带 `operation` / `escalationMode` / `escalationNote` / `approvalTimeoutMs`
- **回合边界**:审批通知只在**回合边界**送达。`open`/`reply` 提交后要**结束本回合**,下一回合才会收到 `app-task-approval-requested`(含 `approvalId`)。在同一个回合里空等或连续重发,会撞上宿主工具回调的 30 秒上限(`RPC callback.tools.execute timed out after 30000ms`),而且该会话可能就此卡住(后续 `reply` 一律超时,`close` 也难得到 DSH 确认);遇到这种会话换新的,不要原地重试
- 审批超时未应答按 `approvalTimeoutSec` 自动拒绝(本 App 缺省 30 秒;显式设 0 则禁用自动拒绝)。注意宿主自身的 `timeoutMs` 默认是 0(不禁用即不超时)——30 秒是 App 侧策略
### 典型用法
- 开活:`open`(新任务)或 `reply`(往已有子代理续;先 `get` 确认)
- 回看:`get`(最终结论)
- 止损:`close`;越界权限:`approve`(提交后先让出回合,审批通知下一回合才到)
`sessionId` 即访问凭证;`get` 的取数走受管 runtime 的官方查询面,不读会话文件、不发起推理,DSH 未启动时不可用。
## 改完源码之后(开发循环)
三件工具都在仓库里,别再造临时脚本:
| 要干什么 | 命令 |
|---|---|
| 把本地包装进宿主(卸载 → 提交 staging → 确认 → 等就绪,跨平台) | `pnpm run install:local -- --zip releases/<包>.zip` |
| 体检**装好的**那棵树(预检:依赖就位 + 定位 DSH + 产物在位) | `pnpm run smoke:packed -- --preflight` |
| 同上但不加 `--preflight`:完整 boot 到中继有应答 | `pnpm run smoke:packed` |
| 查宿主能力面:应用能调哪些 bus 动词、不能用哪些事件名(SDK 已发布契约);某个字面在宿主 bundle 里出现在哪 | `pnpm run probe:host [-- --look models-changed]` |
升级 DSH 或重新装包之后先跑一次 `smoke:packed`:仓库树能过不等于装好的树能过(0.1.6 那次 profile-boot 就是只在装好的树里不合格——哈希产物被压缩,导出名全丢)。
## 主题
只有 DSH 主题偏好为 **system** 时跟随宿主配色(经 `@dshana/theme` 子插件注入);在 DSH 内显式选 light/dark 时完全用 DSH 自己的主题,宿主配色不介入。
外观里这个选项的文案是**「跟随宿主」**(上游原文是「跟随系统」)——偏好值仍是 `system`,只是措辞按我们的形态改了,见 `integrations/ui-theme` 的覆盖层。
## 排错表
| 现象 | 原因 | 处理 |
|---|---|---|
| 卡在「启动中」很久 | 首次 boot 较慢(含插件就位与服务监听) | 等即可;持续不动看宿主日志与 `error.userText`(boot 快照不带日志尾) |
| 状态转「需要处理」 | runtime 启动失败 | 看 `error.userText` 与原始错误;日志定位 |
| 提示端口被占用 | 端口竞争 | 会自动换随机端口重试;持续失败看日志 |
| DSH Web UI 打不开但状态就绪 | 注入失败 / surface 票据缺失 | 重开卡;反复出现查中继前缀与 surface 授权 |
| `dshana` 报 runtime 未就绪 | DSH 还没起来 | 等就绪即可(工具首调会重新拉起);持续失败看 boot 状态与宿主日志 |
| 改了宿主提供商/模型,DSH 里的候选没变 | DSH 侧的 provider 路由与模型目录是启动快照 | 正常路径由 `models-changed` 订阅经控制面触发重拉(不重启 runtime);订阅面不可用时重启 runtime |
- | 模型报 `no API key for provider route "deepseek-official"` | 官方自带的 LLM adapter 还在服务那条路由(本形态里它拿不到 key),说明 roster patch 没随包落地或被人改过 | 确认装好的树 `bin/cordis.patch.yml` 里 `llm-deepseek` / `llm-pi-ai` 是 `disabled: true`,然后重启 DSH |
+ | 模型报 `no API key for provider route "deepseek-official"` | 官方自带的 LLM adapter 还在服务那条路由(本形态里它拿不到 key),说明组合层包没随包落地或被人改过 | 确认装好的树 `node_modules/@dshana/dsh-app/cordis.patch.yml` 里 `llm-deepseek` / `llm-pi-ai` 是 `disabled: true`,然后重启 DSH |
| 默认模型指向宿主没配的提供商/模型(你手设过的那个消失了) | 宿主换过提供商或删了凭据 | 不用手改:App 在 runtime 就绪与宿主模型变更后会对账,换成宿主目录里一条可服务的(日志有「默认模型对账」);这格现在只由 DSH 自己与对账维护,App 设置页没有它的入口 |
| 界面里直接开的会话报 `no API key for provider route "deepseek-official"` | `agent-default-model` 的 user 层为空,值落回 base 层那条官方路由(工具建的会话不受影响:它们按调用方角色卡开) | 在 DSH 自己的模型选择器里选一条可服务的(会话级;App 设置页没有默认模型的入口) |
| 主题没跟随宿主 | DSH 主题偏好是 light/dark 而非 system | 在 DSH 外观里选「跟随宿主」(偏好值 system) |
| DSH 设置里找不到「模型」页 | 该页(`ui-settings-models`)随两个官方 LLM adapter 一起停掉——它只编辑那两行的 settings 段 | 不是故障:工具建会话用的模型在 App 设置页的「会话模型」里配,模型候选列的是宿主目录;DSH 侧会话在 DSH 自己的模型选择器里选 |
| 选工作区目录时弹一个错误 | 目录弹窗落到了 DSH 宿主进程的 OS chooser(要在沙箱里 spawn 子进程开 `IFileOpenDialog`),而本形态的 runtime 是后台子进程 | 正常路径不该走到那里:壳页注入的目录桥让弹窗由宿主出(`hana.resources.pick`)。若仍报错,确认桥装上了(`__DSH_DIRECTORY_PICKER__`)且宿主授予了资源选择 |
| 命令执行里找不到 `bash` 工具 | shell 行按平台互斥挂载:win32 停 `bash` / `bash-sandbox`,只挂 `pwsh` / `pwsh-sandbox` | 用 `pwsh` 工具跑命令(PowerShell);读写文件仍走文件系统工具 |
| `reply` 连续 30 秒超时(`RPC callback.tools.execute`)/ 该会话后续提交全失败 | 上一轮的审批没能在回合边界被应答,宿主工具回调超时,会话卡住 | 不要原地重试:换新会话(`open`);旧会话用 `close` 收敛(回执只说已请求取消,升级标记随后台落定) |
| `close` 回执只说「已请求取消」 | 正常语义:取消确认不进工具回调(后台结算),升级标记随后台落定 | 不用处理;要确认结局看 `get` 或任务通知 |
| 窗口(或卡)里的页停在「历史加载失败」/ `gateway/internal` | 该页绑的受管运行体已重建、中继前缀失效,或 surface 凭据缺失 | 重开该窗口;反复出现看 boot 状态与宿主日志 |
## 已知限制
- **升级 DSH = 装新 App 包 + 重载 App**:DSH 版本由 App 声明的依赖(`@deepseek-ai/dsh`)决定,产物版本段带上它;无独立升级通道。重载会重新 import 服务端入口、重新注册工具与路由,受管 runtime 按自动链重起(不必重启宿主——DSH 跑在受管子进程里,宿主进程内没有它的模块缓存)。一处注意:**已建立的会话**握着上次重建会话状态时解析的工具对象副本,重载后要在那个会话里继续调工具得**压缩上下文**(或开新会话)——工具面会按宿主当前注册表重新解析。
- **壳页没有「启动 / 重启 DSH」的入口**:台面只报状态。拉起由 `apply` 后的自动链、工具首调、以及打开卡页时补的那一次请求承担,失败按退避重试(5s 起、封顶 5min)。要真正重启只能卸载重装、重载 App 或重启宿主。
- **拆窗、钉回、切页面都不停 DSH 后台**:只有卸载/重载 App 或退出 Hana,宿主才回收受管 runtime。
- **数据源固定为 App 内置独立目录**(`<dataDir>/.dsh`,即本形态的 `DSH_HOME`),不碰用户主目录的 `~/.dsh`;共享已有 DSH 目录 / 切换数据源的链未启用(`POST /dshana/settings/restart` 回 503)。
- **会话↔任务的绑定不落 App 文件**:事实源是宿主任务记录(`metadata.dsh` 的 sessionId / rpcId / timeoutSec / approvalTimeoutMs / cancel),读取失败一律 fail-closed。DSH 未启动时 `list` / `get` 不可用。
- **越界权限默认走审批,且只能在新回合被应答**:`open` / `reply` 提交后须结束本回合,审批通知(含 `approvalId`)下一回合才到;同回合内空等会撞上宿主工具回调的 30 秒上限,并可能卡住该会话。`approvalTimeoutSec` 内无人应答自动拒绝(缺省 30 秒;显式设 0 禁用)。DSH Web UI 里直接开的会话没有委派任务,审批请求没有应答者,按 fail-closed 处理。
- **会话流不随任务终结而收线**:页面按 `?sid=` 钉住的那段会话,终态之后照旧可读(历史会话正是用户主动打开来翻看的)。中继侧不设「任务是否活跃」的闸门。
- **模型分两条路**:工具建的会话按调用方角色卡配的模型开(App 设置可改成固定的自定义那条);DSH Web UI 里直接开的会话用 DSH 自己的模型选择器选的那条,候选只来自宿主目录、且是启动快照——宿主改提供商后由 `models-changed` 订阅触发重拉,订阅面不可用时才需要重启 runtime。