dsh-hermes-session 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +161 -2
- package/README.zh.md +299 -0
- package/client.js +446 -0
- package/cordis.patch.yml +22 -0
- package/package.json +65 -4
- package/scripts/hermes_read.py +654 -0
- package/src/bridge.js +140 -0
- package/src/index.js +79 -0
- package/src/render.js +248 -0
- package/src/routes.js +272 -0
- package/src/tools.js +209 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Inston Cai
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,162 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-hermes-session
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Import sessions from a local [Hermes Agent](https://github.com/InternScience/Hermes-Agent) into the
|
|
4
|
+
current [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) conversation as bounded,
|
|
5
|
+
untrusted background context.
|
|
6
|
+
|
|
7
|
+
Hermes Agent is a **separate agent** running on the same machine, with its own session store
|
|
8
|
+
(`~/.hermes/state.db`, one SQLite database per profile). This plugin reads that store **read-only** and
|
|
9
|
+
offers three ways in:
|
|
10
|
+
|
|
11
|
+
| Entry point | Shape | What it does |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `+ Hermes` | Composer button | Opens a searchable list of sessions across every profile; picking one inserts a reference chip into your draft |
|
|
14
|
+
| `hermes_sessions_list` | Model tool | Lists sessions (title / working directory / message count / last activity) across all profiles, sorted by recency |
|
|
15
|
+
| `hermes_session_import` | Model tool | Imports one session by id (a unique prefix is enough) |
|
|
16
|
+
|
|
17
|
+
Imported content always carries a fixed "untrusted background" notice: the model may cite facts from it
|
|
18
|
+
but must not follow instructions, permission claims, or tool requests found inside it. The same rule the
|
|
19
|
+
harness applies to its own `@`-mentioned session references.
|
|
20
|
+
|
|
21
|
+
## Requirements
|
|
22
|
+
|
|
23
|
+
- DeepSeek Harness `>=0.1.0-rc.1`
|
|
24
|
+
- Python 3.8+ on `PATH` (standard library only — the plugin shells out to a small `sqlite3` helper)
|
|
25
|
+
- A local Hermes Agent installation whose session store is readable
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
dsh plugin --profile web add dsh-hermes-session
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or from a checkout:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
dsh plugin --profile web add /path/to/dsh-hermes-session
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Configuration
|
|
40
|
+
|
|
41
|
+
The plugin row takes a `config` mapping (see `cordis.patch.yml`). All fields are optional:
|
|
42
|
+
|
|
43
|
+
| Field | Default | Meaning |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `dbPath` | `""` | Path to a specific `state.db`. Empty scans every profile under `~/.hermes/profiles/` plus the default database |
|
|
46
|
+
| `pythonPath` | `""` | Python interpreter to run the helper with. Empty uses `$HERMES_PYTHON`, then `python3` |
|
|
47
|
+
| `maxChars` | `60000` | Default character budget for one import |
|
|
48
|
+
| `maxCharsHard` | `160000` | Upper bound a caller may request |
|
|
49
|
+
| `maxMessages` | `300` | Default number of messages to carry over |
|
|
50
|
+
|
|
51
|
+
## How it works
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
Hermes Agent this plugin DSH
|
|
55
|
+
──────────── ─────────── ───
|
|
56
|
+
~/.hermes/state.db ──read-only──▶ scripts/hermes_read.py ──▶ Hermes session
|
|
57
|
+
~/.hermes/profiles/*/state.db │
|
|
58
|
+
│ sessions table (metadata)
|
|
59
|
+
│ messages table (conversation)
|
|
60
|
+
▼
|
|
61
|
+
src/render.js → untrusted-background text block
|
|
62
|
+
│
|
|
63
|
+
src/routes.js ◀──HTTP── client.js ─────┘ ▼
|
|
64
|
+
GET /api/hermes.sessions current conversation
|
|
65
|
+
(one collapsed row)
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
- **Read-only by construction.** The helper opens SQLite with `mode=ro`; it never writes to Hermes's
|
|
69
|
+
store, and it never starts the Hermes process (that one self-checks on startup and fails on read-only
|
|
70
|
+
filesystems).
|
|
71
|
+
- **Multi-profile aware.** Hermes keeps one database per profile. The default database holds only the
|
|
72
|
+
`default` profile, so reading it alone would miss every other profile's sessions. The plugin scans all
|
|
73
|
+
of them and labels each result with its source profile.
|
|
74
|
+
- **Child sessions are included.** Hermes hides sessions that continue a parent thread (compaction
|
|
75
|
+
rewrites, sub-agents) unless you ask for them — but those are often exactly the specific stretch of
|
|
76
|
+
work you want to cite. The picker lists them and marks them `子会话` / `sub-session`.
|
|
77
|
+
- **Search runs on the host.** The filter box re-queries the whole store (all profiles, all sessions)
|
|
78
|
+
and only the matches are returned. Filtering a pre-fetched page instead would make sessions that were
|
|
79
|
+
never fetched unsearchable.
|
|
80
|
+
- **Shows the 100 most recent by default.** Sessions are ordered by last activity across every profile,
|
|
81
|
+
newest first. Older ones stay reachable — type in the filter box to search the whole store.
|
|
82
|
+
- **Bounded.** Message bodies are clipped, older messages are dropped first, and the truncation is
|
|
83
|
+
reported in the injected header rather than hidden. The budget is enforced against the **serialized**
|
|
84
|
+
size, so the injected text never exceeds the configured limit.
|
|
85
|
+
|
|
86
|
+
## How the pick reaches the model
|
|
87
|
+
|
|
88
|
+
Picking a session writes a **Markdown mention into your draft** — exactly like the harness's own
|
|
89
|
+
`@`-mentioned session references:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
+ Hermes → pick a session
|
|
93
|
+
→ the draft gets a blue reference chip:
|
|
94
|
+
@[Session title](dsh-hermes:<base64url>)
|
|
95
|
+
→ you type your request and send
|
|
96
|
+
→ agent/pre-step resolves the mention
|
|
97
|
+
→ reads the Hermes session, renders it as a form:'recall' context message
|
|
98
|
+
→ appended after your message → the model sees both in one turn
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The chip is inserted along the same path the native `@` reference uses: bail
|
|
102
|
+
`slash/input-insert-reference` on the session scope, where conversation.input's
|
|
103
|
+
listener hands it to `shell.insertReference`. It therefore renders exactly like the
|
|
104
|
+
harness's own `@`-mentioned session references — blue, removable, and carrying the
|
|
105
|
+
mention in its text.
|
|
106
|
+
|
|
107
|
+
The attached context looks the same too. Its source reuses the native shape
|
|
108
|
+
(`kind: 'session-reference'`, `form: 'recall'`), which is what makes the client
|
|
109
|
+
give it the session-reference icon and the "context recall" title instead of the
|
|
110
|
+
generic injected-context face. Its body is byte-for-byte the native framing: the
|
|
111
|
+
same `## Referenced sessions` heading, the same warning text, the same
|
|
112
|
+
`<referenced-sessions>` wrapper, and the same JSON payload — an **array** of
|
|
113
|
+
session objects whose `conversation` field holds `[{ role, text }]` entries with
|
|
114
|
+
the message text in full. Using a plugin-specific `kind` would work, but it
|
|
115
|
+
would render as a generic injection — same capability, different appearance. And
|
|
116
|
+
because the model-facing prompt has the same shape as the harness's own, the
|
|
117
|
+
model reads a Hermes snapshot the same way it reads a referenced DSH session.
|
|
118
|
+
|
|
119
|
+
Note on the public API: the `inputActions` face a plugin receives exposes only
|
|
120
|
+
`captureInsertion` / `insertText` / `setDraft` / `addAttachments` /
|
|
121
|
+
`removeAttachment` / `pruneAttachments` / `submit` — there is **no chip-insertion
|
|
122
|
+
method** (`insertReference` lives inside the SessionInputShell class). The session
|
|
123
|
+
scope is reachable via `sessions.using(sessionId, ..., ref => ref.binding.ctx)`. The picker
|
|
124
|
+
closes when you click elsewhere or press Escape, and the button is plain text, matching
|
|
125
|
+
the official "+skills" button.
|
|
126
|
+
|
|
127
|
+
Two consequences worth knowing:
|
|
128
|
+
|
|
129
|
+
- **Nothing is attached until you send.** Picking a session never wakes the agent — you choose the
|
|
130
|
+
material first, describe what you want afterwards, and both travel in one message.
|
|
131
|
+
- **The mention stays editable.** It is ordinary draft text: you can reword it, delete it, or add a
|
|
132
|
+
second session, and the model only sees the sessions you actually sent.
|
|
133
|
+
|
|
134
|
+
The mention is parsed by the `agent/pre-step` hook, which reads the session, renders it as a collapsed
|
|
135
|
+
context row (`form: 'recall'`), and appends it after your message.
|
|
136
|
+
|
|
137
|
+
## The injected context
|
|
138
|
+
|
|
139
|
+
The snapshot is attached as a **collapsed context row**, not as an expanded message bubble:
|
|
140
|
+
the conversation shows the source session's title, and expanding it reveals the transcript in a bounded,
|
|
141
|
+
scrollable code-style body. This mirrors how the harness renders its own session references.
|
|
142
|
+
|
|
143
|
+
## Development
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
node --check src/*.js && node --check client.js # syntax
|
|
147
|
+
node selftest/run.mjs && node selftest/clientid.mjs
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The self-tests run against a real Hermes store (read-only) and assert the plugin's invariants: the
|
|
151
|
+
client module id matches the package name, the HTTP routes are registered with the right methods, the
|
|
152
|
+
the mention is parsed and the attached message carries the `form: 'recall'` source shape the chat
|
|
153
|
+
renderer expects.
|
|
154
|
+
|
|
155
|
+
## Privacy
|
|
156
|
+
|
|
157
|
+
The plugin only reads. It never writes to the Hermes store, never sends session content anywhere, and
|
|
158
|
+
runs entirely on the machine where Hermes already lives.
|
|
159
|
+
|
|
160
|
+
## License
|
|
161
|
+
|
|
162
|
+
MIT
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# dsh-hermes-session
|
|
2
|
+
|
|
3
|
+
把本机 **Hermes Agent**(另一个独立智能体)的会话接入 DeepSeek Harness:
|
|
4
|
+
列出会话、指定一个会话,就把它的完整对话作为**不可信背景**注入当前 DSH 会话。
|
|
5
|
+
|
|
6
|
+
## 三个入口
|
|
7
|
+
|
|
8
|
+
| 入口 | 形态 | 作用 |
|
|
9
|
+
|---|---|---|
|
|
10
|
+
| `+ Hermes` | 输入框工具行按钮 | 弹窗列出全部 profile 的会话,选中后插入一个引用 chip 到草稿 |
|
|
11
|
+
| `hermes_sessions_list` | 模型工具 | 列会话(标题 / 目录 / 消息数 / 最后活动),默认扫全部 profile 合并排序 |
|
|
12
|
+
| `hermes_session_import` | 模型工具 | 按 sessionId 导入(支持唯一前缀),注入文本带警示头和截断事实 |
|
|
13
|
+
|
|
14
|
+
`/hermes` 弹窗和工具读的是同一份数据、走同一条注入通道,只是入口不同。
|
|
15
|
+
|
|
16
|
+
**斜杠菜单里只有 `/hermes` 一条**——插件不注册任何 Host 斜杠命令,数据通道走
|
|
17
|
+
HTTP 路由(见下面「数据通道为什么是 HTTP 路由」)。
|
|
18
|
+
|
|
19
|
+
## 架构
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
src/index.js 插件入口:注册工具 + HTTP 路由 + systemPrompt 路由段
|
|
23
|
+
src/tools.js 两个工具的 schema/execute(exec.deferContext 注入)
|
|
24
|
+
src/routes.js Host 端 HTTP 路由:GET /api/hermes.sessions(列表)、
|
|
25
|
+
GET /api/hermes.sessions(列会话)
|
|
26
|
+
client.js 客户端半部:注册 CommandContribution(popupSelect + icon),
|
|
27
|
+
options/onSelect 用 fetch 调上面两条路由
|
|
28
|
+
src/bridge.js 子进程调用 hermes_read.py(--db/--profile 前置,避免 argparse 报错)
|
|
29
|
+
src/render.js 投影 → 注入文本(警示头 + 元信息 + 预算截断)
|
|
30
|
+
scripts/hermes_read.py 只读访问 SQLite,Python 3.8+ 标准库,零第三方依赖
|
|
31
|
+
selftest/run.mjs 不经过 harness 的端到端自测(工具 + HTTP 路由)
|
|
32
|
+
selftest/clientid.mjs 客户端模块不变量(id === 包名、走路由、图标、无 Host 命令)
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### /hermes 弹窗的数据流
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
浏览器输入 /hermes
|
|
39
|
+
→ CommandContribution (client.js) 匹配
|
|
40
|
+
→ popupSelect.options()
|
|
41
|
+
└─ fetch('api/hermes.sessions')
|
|
42
|
+
← Host 路由返回 JSON → 解析成 SelectOption[]
|
|
43
|
+
→ 用户点击某条
|
|
44
|
+
→ 草稿里写入 @[标题](dsh-hermes:<base64url>) ← inputActions.insertText()
|
|
45
|
+
→ 用户写描述并发送
|
|
46
|
+
→ agent/pre-step 解析提及 → 读会话 → 追加 form:'recall' 上下文
|
|
47
|
+
→ 对话里出现一行可折叠的上下文
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### 菜单里的命令与图标
|
|
51
|
+
|
|
52
|
+
`/hermes` 是**客户端 contribution**(`CommandContribution`),带 `icon:
|
|
53
|
+
IconArchiveOutlineRegular`(归档箱)。图标来自
|
|
54
|
+
`@deepseek-ai/dsh-client-ui-primitives`——它是 Web shell 的平台 seed
|
|
55
|
+
(`staticModules`),任何 client 模块 `require` 即得,无需在 `dsh.client` 里声明。
|
|
56
|
+
|
|
57
|
+
`/compact` `/model` `/export` 的图标走的是另一条路:`dsh-client-ui-commands` 里的
|
|
58
|
+
`BUILTINS` 表把 `definitionId` 映射到命令名再取 `HOST_FACES` 的图标——
|
|
59
|
+
那张表硬编码在 DSH 内部,外部插件的 Host 命令拿不到。
|
|
60
|
+
|
|
61
|
+
### 数据通道为什么是 HTTP 路由
|
|
62
|
+
|
|
63
|
+
试过三条路,只有第三条同时满足「有图标」和「菜单只有 /hermes 一条」:
|
|
64
|
+
|
|
65
|
+
1. **自定义 Remote 服务**(`TypertRemoteService` + `@Remote`):Host 的 Gateway
|
|
66
|
+
能发现,但**客户端不会出现 `remote.hermes`**——客户端的 `ctx.remote.<ns>`
|
|
67
|
+
由 DSH 自带的 `dsh-api-remotes` **逐个手工声明**(`commands` / `account` /
|
|
68
|
+
`agentPresets` / `productAnalytics` …),**没有自动发现机制**。客户端 half 永远
|
|
69
|
+
`pending (waiting for service: remote.hermes)`,boot 审计判失败,harness 起不来。
|
|
70
|
+
2. **斜杠命令做数据通道**(`ctx.commands.register` + `ctx.remote.commands.execute`):
|
|
71
|
+
能通,但 **Host 命令无法从菜单隐藏**——`CommandDefinition` 没有 hidden/visible
|
|
72
|
+
字段,客户端 `candidates()` 直接展开 Host catalog(无过滤钩子),每注册一条
|
|
73
|
+
菜单就多一项。而图标只给客户端 contribution(`CommandContribution` 有 `icon`
|
|
74
|
+
字段,`CommandDecoration` 没有),且 Host 命令与 contribution **不能同名**
|
|
75
|
+
(同名会让 `candidates()` throw,整个菜单坏掉)。
|
|
76
|
+
3. **HTTP 路由**(`ctx.connection.fetch.register`):Host 注册 `/api` 下的精确路由,
|
|
77
|
+
客户端直接 `fetch`。**路由不进斜杠菜单**,也不需要任何 Remote 命名空间。
|
|
78
|
+
|
|
79
|
+
第 3 条有现成范例:`dsh-session-log-export` 的 `/export` 就是
|
|
80
|
+
`ctx.connection.fetch.register({ path: '/api/session.export', ... })` + 客户端 fetch。
|
|
81
|
+
鉴权由物理载体处理("after the physical carrier has applied its trust and
|
|
82
|
+
authentication policy"),路由本身不用管。
|
|
83
|
+
|
|
84
|
+
代价:多开两个 HTTP 端点(只读列表 + 只读导入),都在 `/api/hermes.*` 下。
|
|
85
|
+
|
|
86
|
+
### 注入时机与形态:写进草稿,随消息一起发送
|
|
87
|
+
|
|
88
|
+
用户选完 Hermes 会话时**不能**直接注入——他还没说要做什么,直接注入会唤醒
|
|
89
|
+
agent 让模型自己开跑(这正是被反馈过的问题)。
|
|
90
|
+
|
|
91
|
+
对齐 DSH 原生 `@` 提及会话的做法,也对齐官方「+技能」按钮的实现:**把一条
|
|
92
|
+
Markdown 提及写进草稿**,用户写完描述一并发送。
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
+ Hermes 选中会话
|
|
96
|
+
→ 草稿里出现一个蓝色引用 chip:@[会话标题](dsh-hermes:<base64url>)
|
|
97
|
+
→ 用户在输入框写描述并发送
|
|
98
|
+
→ agent/pre-step 钩子解析这个提及
|
|
99
|
+
→ 读 Hermes 会话 → 渲染成 form:'recall' 的上下文消息
|
|
100
|
+
→ 追加到该轮用户消息之后 → 模型这一轮两者一起看
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
chip 走**原生 `@` 引用同一条路径**插入:在会话作用域上 bail
|
|
104
|
+
`slash/input-insert-reference` 事件,由 conversation.input 的监听器转交给
|
|
105
|
+
`shell.insertReference`,所以样式和原生 `@` 会话引用完全一致——蓝色、可删除、
|
|
106
|
+
文本里带着提及。
|
|
107
|
+
|
|
108
|
+
附带的上下文**长得也一样**:source 直接复用原生形状
|
|
109
|
+
(`kind: 'session-reference'` + `form: 'recall'`)——客户端只有看到这个 kind 才会给
|
|
110
|
+
会话引用图标和 "contextRecall" 标题,否则退化成通用注入图标;正文也与原生
|
|
111
|
+
逐字同构:同样的 `## Referenced sessions` 标题、同一段警示语、同样的
|
|
112
|
+
`<referenced-sessions>` 包裹,JSON 载荷是**数组**,每个会话对象里的
|
|
113
|
+
`conversation` 字段是 `[{ role, text }]`,text 是完整原文。
|
|
114
|
+
用自己的 kind 也能用,但会渲染成"通用注入"——同样的能力,两种长相。
|
|
115
|
+
因为模型侧看到的提示结构与引用 DSH 会话时一致,模型读 Hermes 快照的方式
|
|
116
|
+
和读 DSH 会话快照的方式也一致。
|
|
117
|
+
|
|
118
|
+
注意:公开给插件的 `inputActions` 只有 `captureInsertion` / `insertText` /
|
|
119
|
+
`setDraft` / `addAttachments` / `removeAttachment` / `pruneAttachments` / `submit`
|
|
120
|
+
——**没有插 chip 的方法**(`insertReference` 只存在于 SessionInputShell 类内部)。
|
|
121
|
+
拿会话作用域 ctx 的方式:`sessions.using(sessionId, ..., ref => ref.binding.ctx)`。
|
|
122
|
+
|
|
123
|
+
弹窗点外部或按 Esc 关闭;按钮是纯文字,与官方「+技能」按钮一致。
|
|
124
|
+
|
|
125
|
+
两个直接后果:
|
|
126
|
+
|
|
127
|
+
- **不发消息就不附加任何东西。** 选会话永远不会唤醒 agent。
|
|
128
|
+
- **提及是普通草稿文本**,可改写、可删除、可再加一个会话——模型只看到你真正发出去的。
|
|
129
|
+
|
|
130
|
+
草稿写入用的是 session slot 的标准 `inputActions` 面
|
|
131
|
+
(`captureInsertion()` + `insertText()`):span 必须在插入时刻取,从 React prop
|
|
132
|
+
里读的永远滞后一版,会被编辑器的 span-CAS 静默拒绝——弹窗关掉但草稿没变。
|
|
133
|
+
|
|
134
|
+
(工具 `hermes_session_import` 走的是另一条路——它在 turn 内执行,
|
|
135
|
+
`exec.deferContext()` 的上下文挂在本次工具结果上,loop 在 `tool/result` 之后
|
|
136
|
+
追加,天然可见,不受这里影响。)
|
|
137
|
+
|
|
138
|
+
### 注入在对话里长什么样
|
|
139
|
+
|
|
140
|
+
注入消息的 `source` 用 DSH 的 **recall 形态**:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
source: {
|
|
144
|
+
kind: 'hermes-session',
|
|
145
|
+
form: 'recall',
|
|
146
|
+
version: 1,
|
|
147
|
+
references: [{ sessionId, label, retainedMessages, omittedMessages, truncated }],
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`form: 'recall'` 让 DSH 的 `ContextInjectionRow` 渲染它——对话列表里是**一行可折叠的
|
|
152
|
+
上下文**(折叠时显示来源标签,展开后是代码块样式的正文,max-height 141px 可滚动),
|
|
153
|
+
而不是把全文铺开的 user 气泡。契约见 `dsh-client-ui-chat` 的 `recalledSessions()`:
|
|
154
|
+
`references[]` 每项必须带 `label` / `retainedMessages` / `omittedMessages` / `truncated`。
|
|
155
|
+
|
|
156
|
+
这与 DSH 原生 `@` 引用会话是同一种呈现——语义也吻合("从另一个会话恢复的上下文")。
|
|
157
|
+
|
|
158
|
+
### 数据来源(已实机核实)
|
|
159
|
+
|
|
160
|
+
Hermes 的会话按 **profile 分库**:
|
|
161
|
+
|
|
162
|
+
- `~/.hermes/state.db` → profile `default`(本机 270 个会话)
|
|
163
|
+
- `~/.hermes/profiles/<name>/state.db` → 其它 profile(本机 geek/ngs/paper_reader/prodes/sirna 共 153 个)
|
|
164
|
+
|
|
165
|
+
主库**只有 default** 的会话;只读主库会漏掉其它 profile。所以默认扫全部库,
|
|
166
|
+
`--profile` / 工具的 `profile` 参数可限定。每条结果都标注来源 profile。
|
|
167
|
+
|
|
168
|
+
会话元信息在 `sessions` 表,消息在 `messages` 表(OpenAI 风格:user / assistant(含
|
|
169
|
+
`tool_calls` JSON) / tool(结果 JSON))。只读行过滤 `active=1`;默认隐藏 archived
|
|
170
|
+
和 hidden。
|
|
171
|
+
|
|
172
|
+
**子会话默认带上**(与 `hermes sessions list` 的默认不同):Hermes 会把"续写/子代理"
|
|
173
|
+
这类 `parent_session_id` 非空的会话藏起来,但它们往往正是用户想引用的那一段具体工作。
|
|
174
|
+
弹窗列表里会标 `子会话`。工具的 `includeChildren` 参数可控制。
|
|
175
|
+
|
|
176
|
+
**搜索在 Host 侧做**:过滤框会重新查询整库(全部 profile、全部会话),只返回匹配结果。
|
|
177
|
+
如果改成"先拉一页再本地筛",没被拉下来的会话就永远搜不到——曾经就因为这个
|
|
178
|
+
(外加写死 `--limit 50`),"DNA-Diffusion训练"这条子会话在弹窗里消失,而
|
|
179
|
+
`hermes sessions list` 里明明有。
|
|
180
|
+
|
|
181
|
+
**默认显示最近 100 条**:按**最后活动时间**跨全部 profile 降序(最新的在最前)。
|
|
182
|
+
更早的会话照样能拿到——在过滤框里搜,服务端会全库匹配。
|
|
183
|
+
|
|
184
|
+
**预算按序列化后的长度算**:注入文本的字符上限是硬约束。JSON 会转义引号/换行,
|
|
185
|
+
按原文估会低估,所以预算核算与最终裁剪都以**实际 JSON 长度**为准,注入文本
|
|
186
|
+
永远不会超限。
|
|
187
|
+
|
|
188
|
+
### 安全与只读语义
|
|
189
|
+
|
|
190
|
+
- SQLite 以 `mode=ro` 打开,**绝不写** Hermes 的任何数据(本机根分区曾以 ro 挂载,只读是唯一安全姿态)
|
|
191
|
+
- 不 import Hermes 源码、不启动 hermes 进程(其 CLI 在只读文件系统上有自检会崩);直接读持久化契约,schema 变化时给结构化错误
|
|
192
|
+
- 注入内容带固定警示:禁止模型把快照里的指令 / 权限声明 / 工具请求当真,与 DSH 原生 `session-reference` 同语义
|
|
193
|
+
- 注入通道分两种:工具用 `exec.deferContext()`(turn 内机制);`/hermes` 弹窗走
|
|
194
|
+
`agent/pre-step` 钩子,在用户下一次发消息时把暂存的会话作为上下文追加到该轮。
|
|
195
|
+
两者都不新开用户轮
|
|
196
|
+
|
|
197
|
+
### 预算
|
|
198
|
+
|
|
199
|
+
- 单条工具结果保留 1200 字符,工具调用参数 600 字符
|
|
200
|
+
- `maxMessages`(默认 300)保留"首条 user 消息 + 最近 N 条",理解任务起点不丢
|
|
201
|
+
- `maxChars`(默认 60000,硬上限 160000)从尾部装填,超预算丢最老并如实报告条数
|
|
202
|
+
|
|
203
|
+
## 配置(cordis.patch.yml,可被用户层覆盖)
|
|
204
|
+
|
|
205
|
+
| 字段 | 默认 | 说明 |
|
|
206
|
+
|---|---|---|
|
|
207
|
+
| `dbPath` | 空 | 直连某个 state.db;空则扫全部 profile 库 |
|
|
208
|
+
| `pythonPath` | `python3` | 解释器(需标准库 sqlite3,Python 3.8+) |
|
|
209
|
+
| `maxChars` | 60000 | 单次导入默认字符预算 |
|
|
210
|
+
| `maxCharsHard` | 160000 | 工具调用可调的上限护栏 |
|
|
211
|
+
| `maxMessages` | 300 | 默认最多带多少条消息 |
|
|
212
|
+
|
|
213
|
+
## 开发与自测
|
|
214
|
+
|
|
215
|
+
```bash
|
|
216
|
+
node --check src/*.js && node --check client.js # 语法
|
|
217
|
+
node selftest/run.mjs # 28 项端到端自测(读真实库,零写入)
|
|
218
|
+
python3 scripts/hermes_read.py list --limit 5
|
|
219
|
+
python3 scripts/hermes_read.py --profile sirna show 20261021_135400_6c2813
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
自测覆盖:工具全路径、跨 profile、HTTP 路由与 pre-step 钩子(暂存→注入、取出即消费)、错误路径。
|
|
223
|
+
|
|
224
|
+
本地跑 selftest 需要解析 `@deepseek-ai/dsh-llm` / `dsh-tools` / `dsh-typert-protocol`
|
|
225
|
+
(开发目录软链,见 `node_modules/@deepseek-ai/`;装进 profile 后由 harness 解析器提供)。
|
|
226
|
+
|
|
227
|
+
## 安装状态
|
|
228
|
+
|
|
229
|
+
- 已装入 `web` profile(link 安装指向本目录),`include:dsh-hermes-session` 行 active
|
|
230
|
+
- **`dsh.client` 不设 `immediately: true`**:boot 只审计 `immediately` 的 client rows,
|
|
231
|
+
懒加载(用户输入 `/hermes` 才 import)与 `/model` 同模式,启动失败面最小
|
|
232
|
+
- **源码改动需重启 harness 进程才在运行时生效**:`set_plugin` disable/enable 只重放
|
|
233
|
+
fiber 生命周期,Node 的 ESM 模块缓存不刷新——重启前进程持有加载时的旧代码副本
|
|
234
|
+
- 浏览器验证受限:本环境无 X server(electron 报 `Missing X server or $DISPLAY`),
|
|
235
|
+
dsh_browser_* 工具不可用;`/hermes` 弹窗需在有显示环境的机器上验证
|
|
236
|
+
|
|
237
|
+
### 踩坑记录:client 面让 harness 无法启动
|
|
238
|
+
|
|
239
|
+
第一次带 `dsh.client` 安装后,harness 启动失败:
|
|
240
|
+
`Failed to load plugins` → `web boot: 1 entry did not activate` →
|
|
241
|
+
`dsh-hermes-session: import failed (see console for the import error)`。
|
|
242
|
+
|
|
243
|
+
**根因**:`client.js` 里 `window.__ModuleLoader__.load({ id: ... })` 的 id 写成了
|
|
244
|
+
`@deepseek-ai/dsh-hermes-session`,而包名是 `dsh-hermes-session`(无 scope)。
|
|
245
|
+
|
|
246
|
+
模块系统按 graph row 的 id(= 包名)查找已注册的 factory:id 对不上 →
|
|
247
|
+
bundle 加载了但 "loaded without registering" → fiber 建不起来 →
|
|
248
|
+
`entry.fiber === undefined` → client-modules 抛 `import failed` →
|
|
249
|
+
boot audit 判失败 → **整个 harness 起不来**。
|
|
250
|
+
|
|
251
|
+
对照实现 `dsh-client-ui-model-selection` 的包名与 client id 严格相等。
|
|
252
|
+
|
|
253
|
+
**修复**:`client.js` 的 id 改为 `dsh-hermes-session`(= package.json 的 name)。
|
|
254
|
+
`selftest/clientid.mjs` 断言两者相等,防止再犯。
|
|
255
|
+
|
|
256
|
+
**附带**:`dsh.client` 不设 `immediately: true`(对齐 `/model` 的懒加载模式)——
|
|
257
|
+
`/hermes` 是用户主动触发的命令,不需要 boot 时激活,进一步缩小启动失败面。
|
|
258
|
+
|
|
259
|
+
### 踩坑记录:命令没出现在斜杠菜单
|
|
260
|
+
|
|
261
|
+
id 不匹配修好后,若 `/hermes` 仍作为普通消息进入模型,说明 client 模块没被 import
|
|
262
|
+
(`commandUi.register` 从未执行)。此时查浏览器 console 里
|
|
263
|
+
`client-modules: could not load "dsh-hermes-session"` 的具体原因。
|
|
264
|
+
|
|
265
|
+
### 踩坑记录:自定义 Remote 命名空间不可用(pending 等不到服务)
|
|
266
|
+
|
|
267
|
+
id 修好后错误变成:
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
web boot: 1 entry did not activate
|
|
271
|
+
dsh-hermes-session: pending (waiting for service: remote.hermes)
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
**根因**:客户端的 `ctx.remote.<ns>` 命名空间由 `dsh-api-remotes` 逐个手工声明,
|
|
275
|
+
没有自动发现机制。Host 侧 `new TypertRemoteService(ctx, 'hermes')` + `@Remote` 标记
|
|
276
|
+
只能让 **Host 的 Gateway** 发现它,**客户端不会出现 `remote.hermes`**——
|
|
277
|
+
client half 永远等不到这个服务,boot 审计判 pending → 整个 harness 起不来。
|
|
278
|
+
|
|
279
|
+
**修复**:改走 HTTP 路由——Host 侧 `ctx.connection.fetch.register()` 注册
|
|
280
|
+
`/api/hermes.sessions`,客户端直接 `fetch`。
|
|
281
|
+
(中途试过 `ctx.remote.commands` 通道,但 Host 命令无法从菜单隐藏,见上面
|
|
282
|
+
「数据通道为什么是 HTTP 路由」。)
|
|
283
|
+
|
|
284
|
+
`selftest/clientid.mjs` 断言客户端代码里不出现 `remote.hermes`、不出现
|
|
285
|
+
`remote.commands`,且插件没有 `src/commands.js`——防止回退。
|
|
286
|
+
|
|
287
|
+
## 已知边界
|
|
288
|
+
|
|
289
|
+
- `hermes sessions list` CLI 本身在本机不可用(只读文件系统导致其自检崩溃),插件绕过 CLI 直读数据库
|
|
290
|
+
- 快照是 `get_messages_as_conversation` 语义的简化投影:不解 hermes 的私有 marker 列,遇到非文本 content 按原文处理
|
|
291
|
+
- 压缩续写(compression lineage)不跟随:导入的是该会话自身记录的活跃行
|
|
292
|
+
- 斜杠菜单里只有 `/hermes` 一条;数据通道是 `/api/hermes.sessions` 与
|
|
293
|
+
`/api/hermes.sessions`(只读 Hermes 库,不写)
|
|
294
|
+
- 注入的上下文在对话里是**一行可折叠的上下文**(`form: 'recall'`),全文不铺开;
|
|
295
|
+
选中会话只是往草稿写一行提及,本身不触发任何对话——上下文随你下一条消息一起进入模型请求
|
|
296
|
+
- 插件硬依赖 `agents` 服务(注入要 `ctx.agents.get(sessionId)` 定位当前会话);
|
|
297
|
+
漏声明会让 `ctx.agents` 为 undefined,导入直接 400
|
|
298
|
+
- 注入走 `agent.steer()`(唤醒 driver);用 `inject()` 的话,会话空闲时消息
|
|
299
|
+
不会被消费,接口 200 但对话里看不到
|