# dsh-opencode-session
**Repository Path**: Macbook-Specter/dsh-opencode-session
## Basic Information
- **Project Name**: dsh-opencode-session
- **Description**: dsh 插件:为来自 dsh 会话 ID 的 OpenCode Go(opencode.ai)LLM 请求附加一个稳定的、按会话的 x-opencode-session 头部
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2026-09-10
- **Last Updated**: 2026-09-13
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
dsh-opencode-session
为 DeepSeek Harness 的 OpenCode Go 请求自动携带每会话稳定的 x-opencode-session 头,消除 MissingSessionID。

## 能力面
| 机制 | 作用 |
|---|---|
| bundle 层(`dsh.bundle.patch`) | 一条 `dsh plugin add` 完成安装与挂载:profile 自动追加层,**不需要手写任何配置** |
| `llm/stream` waterfall 监听 | 对配置的 provider 路由(默认 `go`、`opencode-go`),把 dsh 会话 ID 发布到 AsyncLocalStorage 载体 |
| `globalThis.fetch` 包装 | 请求发出时对匹配 host(默认 `opencode.ai`)注入 `x-opencode-session: `,其余流量零改动 |
| `opencode-session` 设置段 | providers / hosts / headerName / debug 全部可配,设置 HMR 实时生效,无需重启 web(可选依赖:无设置提供方时缺席,不影响注入) |
## 原理
OpenCode Go(OpenCode Zen 的 Go 通道,`https://opencode.ai/zen/go/v1`)要求每个请求携带稳定的 `x-opencode-session` 头用于会话路由与提示词缓存;缺失时拒绝请求(`MissingSessionID`)。dsh 的 pi-ai 适配器在 wire 上没有该头的通道(会话亲和头默认关闭且头名不同),因此本插件在两条既有接缝上搭桥:
```
agent loop ──(options.sessionId 必带)──▶ llm/stream waterfall ──▶ AsyncLocalStorage
│
模型请求 fetch(opencode.ai) ◀── 注入 x-opencode-session ◀── 读载体 ◀─┘
```
- **会话 ID 来源**:dsh agent loop 强制每个模型请求携带 `options.sessionId`(每段对话唯一、跨请求稳定),不需要新生成任何标识。
- **作用域传播**:dsh 的适配器流是**惰性 async generator**——真正发起 fetch 的执行体在消费者迭代时才运行,远在 waterfall `next()` 返回之后。因此监听器不仅把 `next()` 放进 AsyncLocalStorage 作用域,还把返回的流包装成**每个迭代步骤都重新进入作用域**的代理(`scopeStream`),否则 store 在 fetch 时早已为空(首次实测正是败在这里)。
- **注入位置**:OpenAI SDK(pi-ai 底层,openai@6)在客户端构造时取当时的 `globalThis.fetch`;客户端在每次流式调用中构造,包装器只要在启动早期装好即始终在链上。包装器永不抛错、永不阻断请求,注入失败时按原样放行。
- **生命周期**:插件卸载(disable / profile 卸载)时还原原 `fetch` 并摘除监听;重复挂载基于裸 `fetch` 包装,不会叠加多层。
## 安装(一条命令)
```bash
dsh plugin --profile web add D:\me\wordspase\dsh-opencode-session
```
就这一步。仓库通过 `package.json` 的 `dsh.bundle.patch` 声明自己是 **bundle**:`dsh plugin add` 除了 pnpm link,还会把这个包追加进 profile 的 `dsh.profile.bundles`;启动时组合器自动套用仓库自带的 `cordis.patch.yml` 层,插入 `dsh-opencode-session` 行。**profile 的 `cordis.patch.yml` 保持原样,不需要手写 insert 行,也不需要在仓库里先跑 `pnpm install`。**
- **生效时机**:bundle 层在 profile **启动时**组合 → 重启 `dsh web` 后生效(用户层 `cordis.patch.yml` 是 live HMR,`dsh.profile.bundles` 不是)。
- **安装后核对**(不启动服务):
```bash
dsh --profile web --dump-config | Select-String dsh-opencode-session
```
应出现 `# == dsh-opencode-session` 层与 `- id: dsh-opencode-session` 行。
- **卸载**:`dsh plugin --profile web remove dsh-opencode-session` —— 依赖与层一起移除,profile 恢复原状。
- **改代码**:`link:` 安装保持源码直连,改完插件源码重启 `dsh web` 即用新代码(只有改 row 的 `config` 才走设置 HMR,不需要重启)。
- **旧文档的手写行**:若此前按旧说明在 profile 的 `cordis.patch.yml` 里加过 `- insert: … id: dsh-opencode-session`,删掉它即可;同 id 的行会被用户层覆盖,不会挂载两次,但留着徒增混淆。
## 依赖与 schema 解析(为什么 link 安装也能直接跑)
设置段要用宿主提供的 `@deepseek-ai/schemastery` 描述配置。`link:` 安装只把仓库符号链接进 profile,pnpm 不会代装被链接包的依赖;而 Node 解析符号链接走的是**真实路径**,父目录链到不了 profile,插件的裸 `import` 必然失败——这正是旧版本要求「仓库先 `pnpm install`」的原因,也是「装完还要手改配置」之外的第二处手工步骤。
现在改为运行期解析(`lib/schema.mjs`),按宿主设计的顺序尝试两条路:
| 顺序 | 路径 | 适用场景 |
|---|---|---|
| ① | `import("@deepseek-ai/schemastery")` | 仓库自带 node_modules(`pnpm install`)、registry / tarball / `file:` 安装 |
| ② | `$DSH_HOME/profiles/node_modules/@deepseek-ai/schemastery` | `link:` 安装的仓库——dsh 把整个安装闭包投影在 `profiles/` 目录,正是 profile 的父级(`dsh-app-boot` 的 profile module fallback) |
两条都不可用时**降级而非崩溃**:跳过设置段(设置卡片消失),核心的会话头注入照常工作,并在 `$DSH_HOME/opencode-session.log` 写明两次失败原因。于是 `link:` 装的仓库无需 `pnpm install`,安装只剩一条命令。
设置服务同样是**可选依赖**:插件行不声明 `inject`,设置段走 `ctx.inject(["settings"], …)`(与 dsh 自家的 `dsh-llm-pi-ai` / `dsh-llm-deepseek` 一致)。插件要做的只有「监听 `llm/stream` + 包装 `fetch`」,两者不需要任何注入;把 `settings` 写进插件级 `inject` 反而会在没有设置提供方的 profile 上把整个插件停在 PENDING —— 什么都不挂、什么都不注入,而且不报错。
解析发生在模块顶层(`Config` 必须在 loader 建 runtime 时就可读,`apply` 里再赋值已经太晚),因此 `Config` 在两种情形下分别是真实的 schemastery schema 或 `undefined`;后者由 loader 解释为「无 schema」,row 的组合配置原样传入,`resolveConfig` 依旧兜住所有默认值。
## 配置
设置段 `opencode-session`(与 `llm-pi-ai` 等并列):
| 字段 | 默认 | 说明 |
|---|---|---|
| `providers` | `["go", "opencode-go"]` | 哪些 llm-pi-ai provider 路由携带会话头;显式 `[]` 关闭 |
| `hosts` | `["opencode.ai"]` | 匹配的请求 host(精确或子域);显式 `[]` 关闭 |
| `headerName` | `x-opencode-session` | 服务端要求的头名 |
| `debug` | `false` | 每次注入额外打一行终端 info 日志(会话 ID 截前 8 位) |
```yaml
opencode-session:
providers: [go, opencode-go]
hosts: [opencode.ai]
debug: true
```
配置有三条入口,优先级从高到低:设置卡片(live HMR)→ profile 的 `cordis.patch.yml` 按 id 覆盖 → bundle 层自带的默认 row。
## 诊断日志
无论 `debug` 开关,插件都会把挂载/卸载标记和每次请求的注入决策追加到
`$DSH_HOME/opencode-session.log`(默认 `~/.dsh/opencode-session.log`)——
web 终端没人常开,文件才是事后可查的通道:
| 日志行 | 含义 |
|---|---|
| `… opencode-session/0.1.2 mounted pid=… schema=fallback:…` | 插件已在当前 web 进程挂载(附版本与 schema 解析路径,可辨识新旧代码代次) |
| `… x-opencode-session=xxxxxxxx… → opencode.ai` | 本次请求已注入会话头 |
| `… skip opencode.ai (no session in async scope)` | 命中 host 但当前异步作用域没有会话(如模型发现 GET) |
| `… settings section unavailable — …` | 两条 schema 解析路径都失败,插件已按降级模式挂载 |
## 验证
1. 安装后重启 `dsh web`,看启动日志没有 `plugin tree failed to load`。
2. 用 `go` provider 的模型(如 omen-alpha)发一条消息,不再出现 `MissingSessionID`。
3. 看 `~/.dsh/opencode-session.log`:应有 `mounted` 标记与每请求的 `x-opencode-session=… → opencode.ai` 注入行。
4. 需要终端同步观察时临时开 `debug: true`。
## 门禁与自测
```bash
npm run gates # 语法检查(6 个模块)+ bundle 契约 + 24 项自证测试(纯 Node,无需 dsh 运行时)
```
- **bundle 契约**(`scripts/bundle-contract.mjs`):`dsh.bundle.patch` 指向的文件存在、层里插入的 row 名字就是包名、`files[]` 会随包发出入口与 patch、版本号与运行期 `GENERATION` 标记一致。安装故事全是「文件声明」——没有运行期会因此报错,所以单独设一道门禁盯住它。
- **自测**(`scripts/selftest.mjs`):核心逻辑(配置归一、host 匹配、waterfall、真实 HTTP 服务器上的注入、幂等、re-entrancy)+ schema 解析两条路径(用临时 `$DSH_HOME` 下的 stub 包,不依赖本机 dsh)+ 入口模块在「有 schema」「无 schema 降级」「无设置提供方」三种形态下的 `apply`/dispose 全流程。
## 插件管理
已装插件用 plugin-registry 的**薄控制台**管理(浏览器面板):管理 profile 插件安装态(bundle 层栈 + insert 行 + 启停),无需手改配置。安装:`dsh plugin --profile web add /packages/plugin/console`
## 决策记录
- [2026-09-09 设计与方案取舍](docs/superpowers/specs/2026-09-09-opencode-session-plugin-design.md) —— 为什么是 fetch 包装而非 waterfall 改参 / 静态头 / 反向代理
- 2026-09-09 装载机制勘误:`dsh plugin add` 对非 bundle 插件只做 pnpm link、**不写 insert 行**(reconcile 只管 `dsh.profile.bundles`);`link:` 包的依赖不由 profile 闭包代装,必须在插件仓库声明并安装。首次实测两个坑都踩了,冒烟测试已回补。
- 2026-09-09 ALS 作用域勘误(真机第二轮实测):插件挂载成功但注入零生效——dsh 适配器流是惰性 async generator,fetch 发生在消费者迭代时,`carrier.run(store, () => next())` 的作用域早已退出。`scopeStream` 改为每个迭代步骤重新进入作用域,回归测试覆盖该形态;同时新增 `$DSH_HOME/opencode-session.log` 文件诊断(挂载标记 + 每请求决策行)。
- 2026-09-13 安装改为「一条命令」(本轮):插件升级为 **bundle**(`package.json` 的 `dsh.bundle.patch` + 自带 `cordis.patch.yml`),`dsh plugin add` 因此自动把层追加进 `dsh.profile.bundles`——profile 的 `cordis.patch.yml` 不再需要手工 insert 行;同时把 schemastery 从静态 import 改为运行期双路解析(自带依赖 → `$DSH_HOME/profiles/node_modules` 安装闭包回退),使 `link:` 安装的仓库**不需要先 `pnpm install`**,两条路都不可用时降级为「无设置段」而不是加载失败;插件级 `inject` 清空(`settings` 改为 `ctx.inject` 可选依赖),避免没有设置提供方时整个插件停在 PENDING。真机验证:`dsh plugin --profile web add` 后 `--dump-config` 出现 `# == dsh-opencode-session` 层,实际启动日志出现 `mounted … schema=fallback:<安装目录>`;新门禁 `scripts/bundle-contract.mjs` 盯住「文件声明」这层契约。