@heixui/dsh-session-suite 0.0.0-stage → 0.3.1

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-session-suite contributors
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,382 @@
1
- # Temporary Holding Version
1
+ # dsh-session-suite
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **By [heixiua](https://github.com/heixiua)** · [github.com/heixiua/dsh-session-suite](https://github.com/heixiua/dsh-session-suite) · MIT
4
+ · npm: [`@heixui/dsh-session-suite`](https://www.npmjs.com/package/@heixui/dsh-session-suite)
5
+
6
+ Session checkpoints for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) (DSH): rewind a
7
+ conversation *together with the workspace files and the session memory it produced*, delete sessions for good,
8
+ share a conversation as an image or a self-contained page — and keep a **long-term memory** that survives across
9
+ sessions and workspaces.
10
+
11
+ It is a DSH bundle (Host + Web Client) built against the desktop profile, and it deliberately targets the
12
+ interaction pattern of Trae's session actions.
13
+
14
+ | Capability | Where it lives |
15
+ |---|---|
16
+ | Rewind to a message (conversation + files + memory) | ↺ button on every final assistant message, inline confirm bar above the composer |
17
+ | Delete one turn (conversation + memory only) | 🗑 button beside ↺ |
18
+ | Delete a chosen run of turns | Session menu → **Delete part of the conversation**, with a turn picker |
19
+ | Delete a session permanently | Session menu → **Delete session** (confirmation dialog) |
20
+ | Share a chosen range | Session menu → **Share**: pick turns, then Image / Web page / Markdown / JSON / copy text |
21
+ | Snapshot management | Session menu → **Session snapshots**, and Settings → **Session snapshots** |
22
+ | Long-term memory (global + per-workspace) | Settings → **Long-term memory**; `【记住:…】`; the `memory` tool |
23
+ | Use WorkBuddy / Trae credits as models | **not this bundle** — see [Using WorkBuddy and Trae credits](docs/credits.md) |
24
+
25
+ ### Long-term memory
26
+
27
+ Two isolated stores (`global` and per-`workspace`), a four-factor importance score
28
+ (frequency · recency · affect · uniqueness), dynamic decay with a cold archive, similarity clustering with
29
+ summary compression, and rule abstraction from repeated tags.
30
+
31
+ The part that matters most: **nothing is stored implicitly without your consent.** After each turn a cheap model
32
+ call proposes durable facts, they queue in the settings page, and only *Keep* writes them. Explicit
33
+ `【记住:…】` / `【新增全局记忆:…】` instructions skip the queue and write immediately.
34
+
35
+ Full detail — scoring formula, thresholds, lifecycle, commands, storage layout — in
36
+ [docs/long-term-memory.md](docs/long-term-memory.md).
37
+
38
+
39
+ ### What each destructive action touches
40
+
41
+ | Action | Conversation | Memory | Workspace files |
42
+ |---|---|---|---|
43
+ | Rewind (↺) | cut at the message | trimmed to that point | **restored** to that turn's snapshot |
44
+ | Delete a turn (🗑) | the prompt, the reply, and the steps between them | trimmed to that point | untouched |
45
+ | Delete part of the conversation | cut at the first selected turn (its prompt included) | trimmed to that point | untouched |
46
+ | Delete session | removed from disk | removed | untouched (the folder stays) |
47
+
48
+ Only a rewind puts files back; every other delete leaves the working tree exactly as it is. A session log is
49
+ append-only, so "cut" always means "keep the prefix and retire the original" — the turns after a removed range go
50
+ with it.
51
+
52
+ ### Sharing
53
+
54
+ The share dialog lists every turn with its prompt and a preview of the answer. Everything is selected by default;
55
+ untick what should stay out. Then choose an output:
56
+
57
+ * **Image** — a PNG of the selected turns, drawn on a canvas (no other library involved);
58
+ * **Web page** — one self-contained HTML file with inline CSS, light/dark aware, printable to PDF;
59
+ * **Markdown** / **JSON** — for pasting into an issue or feeding another tool;
60
+ * **Copy text** — the plain transcript on the clipboard.
61
+
62
+ Both rich formats render the model's Markdown instead of echoing it: headings, bold/italic, inline code, fenced
63
+ code blocks, ordered and bulleted lists, block quotes, pipe tables, and links. The Host parses the log once into
64
+ blocks with inline runs (`lib/markdown.js`), the HTML renderer emits markup from those blocks, and the browser draws
65
+ the very same blocks on a canvas — so the page and the image never disagree.
66
+
67
+ Screenshots travel with the export. Attachments live outside the session log, so the Host resolves them
68
+ (`attachments.imageHostPath`) and inlines them as data URLs under a bounded budget, which keeps the page a single
69
+ portable file. In the image they are drawn at full card width (height-capped, aspect preserved); in the HTML page
70
+ each screenshot is a button that opens a built-in viewer at native resolution — the page carries a small inline
71
+ script and no external request.
72
+
73
+ A raster image cannot contain clickable images: if opening a screenshot at full size matters, share the HTML file
74
+ rather than the PNG. Nothing in the export depends on a network request either way.
75
+
76
+ ### What an export contains
77
+
78
+ A session log is mostly plumbing: runtime context, the skill catalog, time samples, subagent relays, tool calls,
79
+ and the narration a model writes between tools. None of that is the conversation:
80
+
81
+ * only `source.kind === "user"` messages count as prompts — everything else in a user-role event is dropped;
82
+ * **Final answers only** (the default) keeps your prompt plus the closing reply of each turn;
83
+ * untick it to export every assistant step, including the narration around tool calls;
84
+ * tool results never appear in the rich exports, and Markdown/JSON follow the same curation.
85
+
86
+ Consecutive assistant steps of one turn merge into a single card, and steps with no prose are dropped rather than
87
+ exported as empty `Assistant` labels.
88
+
89
+ A public share link would need a server holding the conversation; the web page file is the portable equivalent, so
90
+ that is what this bundle produces instead of minting URLs that would only work on this machine.
91
+
92
+ ## Install
93
+
94
+ From npm:
95
+
96
+ ```bash
97
+ dsh plugin --profile desktop install @heixui/dsh-session-suite
98
+ ```
99
+
100
+ > The published package is scoped because the unscoped name `dsh-session-suite`
101
+ > belongs to a different project on npm (`chc-1986/dsh-session-suite`), not to
102
+ > this one. The scope is only the npm package identity — the plugin id, the
103
+ > storage directory, and the bundle name remain `dsh-session-suite`, so data from
104
+ > earlier builds is still found.
105
+
106
+ From a checkout:
107
+
108
+ ```bash
109
+ # from a checkout of this repository
110
+ dsh plugin --profile desktop install . # or: pnpm add -D . inside the profile
111
+ ```
112
+
113
+ Or let the running Harness install it (this is what `plugin_manager` does):
114
+
115
+ ```js
116
+ // tool call
117
+ plugin_manager({ action: 'install_bundle', target: 'D:/path/to/dsh-session-suite' })
118
+ ```
119
+
120
+ Install once, then reload the page. The bundle registers a single Host row (`session-suite`) whose patch lives in
121
+ `cordis.patch.yml`; the Client half is discovered from `package.json` → `dsh.client`.
122
+
123
+ ## How a rewind works
124
+
125
+ DSH's session log is append-only and is the only source of truth, so a rewind cannot delete events. The suite
126
+ implements it as an atomic three-part operation:
127
+
128
+ 1. **Protect** — capture a temporary *undo* snapshot of the workspace and the memory ledger (10-minute TTL).
129
+ 2. **Conversation** — `sessionController.fork({ sessionId, atSeq })` copies the exact event prefix into a new
130
+ session (inclusive boundary, same `cwd`, same agent preset).
131
+ 3. **Files** — restore the checkpoint recorded for that turn: changed files are rewritten, files the session
132
+ created afterwards are removed, and files that existed before the session are never touched.
133
+ 4. **Memory** — trim memory entries observed after the target sequence.
134
+ 5. **Retire** — archive the source session (hidden from the sidebar, log kept) so nothing is destroyed.
135
+
136
+ `Undo` reverses all of it: files, memory, the archived session, and the forked child.
137
+
138
+ ## Snapshots on disk
139
+
140
+ ```
141
+ %DSH_HOME%/storages/dsh-session-suite/
142
+ sessions/<sessionId>/index.json # checkpoint records
143
+ sessions/<sessionId>/manifests/*.json # path -> {hash, size, mtime}
144
+ sessions/<sessionId>/blobs/<ab>/<hash> # gzip-compressed file contents, content-addressed
145
+ memory/<sessionId>.json # per-session memory ledger
146
+ memory/_global.json # cross-session long-term memory
147
+ undo/<id>.json # rollback undo bundles
148
+ ```
149
+
150
+ * **Incremental** — a manifest stores only tracked paths and hashes; identical content is stored once, and
151
+ unchanged files are skipped using the previous manifest's `(size, mtime)` fingerprint.
152
+ * **Budget** — the newest 10 automatic checkpoints per session are kept; locked checkpoints never expire;
153
+ temporary undo bundles live outside the budget and are swept after 10 minutes.
154
+ * **Ignore list** — `node_modules`, `.git`, caches, build output, logs, lockfiles, and binary files are never
155
+ recorded; `config.ignore` adds your own patterns.
156
+ * **Limits** — files above 2 MiB and trees above 4000 files are skipped rather than slowing a turn down.
157
+
158
+ ## Configuration
159
+
160
+ ```yaml
161
+ - insert:
162
+ - id: session-suite
163
+ name: dsh-session-suite
164
+ config:
165
+ keep: 10 # automatic checkpoints kept per session
166
+ autoCheckpoint: true # capture one at the start of every turn
167
+ maxFileBytes: 2097152
168
+ maxFiles: 4000
169
+ sweepIntervalMs: 300000
170
+ ignore: [] # extra ignore patterns
171
+ ```
172
+
173
+ ## Layout
174
+
175
+ ```
176
+ index.js Host half: route registration, per-turn checkpoints, sweep timer
177
+ client.js Client half: menu rows, message actions, banner, dialog, settings page
178
+ lib/api.js Authenticated Fetch route (POST api/session-suite)
179
+ lib/service.js Session operations: rollback, undo, delete, share, delete-message
180
+ lib/checkpoints.js Content-addressed incremental snapshot store
181
+ lib/memory.js Session memory ledger with rollback trimming
182
+ lib/share.js Session log → Markdown / text / JSON projection
183
+ locale/{en,zh}.json Plugin Manager display metadata and strings
184
+ tools/ Self-tests and the pre-clean helper (not part of the bundle)
185
+ docs/ Requirements and API notes
186
+ ```
187
+
188
+ ## Tests
189
+
190
+ The self-tests exercise the engine and the orchestration against throwaway directories; they never touch the
191
+ real DSH home.
192
+
193
+ ```bash
194
+ node tools/selftest.mjs # snapshots, ignore rules, restore, pruning
195
+ node tools/selftest-rollback.mjs # rollback, undo, delete-message, share, permanent delete
196
+ node tools/selftest-memory.mjs # scoring, decay, clustering, rules
197
+ node tools/selftest-memory-scope.mjs # model fallback chain, workspace isolation
198
+ node tools/selftest-workspace-registry.mjs # workspace registry, automatic maintenance
199
+ node tools/selftest-compaction-boundaries.mjs # cut points survive a compaction
200
+ node tools/selftest-resolve-cwd.mjs # an agent can find its own workspace
201
+ node tools/selftest-maintenance.mjs # idle libraries skip, rules never duplicate
202
+ node tools/selftest-cluster-idempotence.mjs # repeated passes never multiply rules or summaries
203
+ node tools/selftest-inherit.mjs # a forked session keeps its snapshots
204
+ node tools/selftest-retrieval.mjs # BM25, coverage and RRF primitives
205
+ node tools/selftest-wordlist.mjs # longest-match CJK segmentation
206
+ node tools/selftest-factkeys.mjs # fact-key canonicalisation and supersession
207
+ node tools/selftest-factkey-integration.mjs # fact keys through remember and recall
208
+ node tools/selftest-injection.mjs # how many memories get injected, and why not
209
+ node tools/selftest-explicit-writes.mjs # a pipeline message is never obeyed as a user instruction
210
+ node tools/selftest-list-sessions.mjs # a rewound session is still listed; subagents stay hidden
211
+ node tools/selftest-delete-cascade.mjs # cascade delete order, and refuse-before-touching
212
+ node tools/check-locale.mjs # every t() key exists in both dictionaries
213
+ ```
214
+
215
+ Every one of these has been mutation-checked: a deliberate break of the
216
+ behaviour it names makes it fail. That is the only claim worth making about a
217
+ test — one that passes on broken code is documentation, not a guard.
218
+
219
+ `tools/ablate-word-route.mjs` answers a narrower question: does the third
220
+ retrieval route earn its place? It runs each probe twice against the *same*
221
+ store, once with the route and once without, so a shifted memory library cannot
222
+ confound the comparison:
223
+
224
+ ```bash
225
+ node tools/ablate-word-route.mjs
226
+ ```
227
+
228
+ The result is that the word route raises MRR on both the held-out and final sets
229
+ (0.491 → 0.561 and 0.612 → 0.683) while leaving recall@5 unchanged — better
230
+ ordering, no lost hits.
231
+
232
+ Retrieval quality is measured, not asserted. Three probes report the standard
233
+ metrics (recall@k, MRR, nDCG@k) instead of a pass/fail on the top hit:
234
+
235
+ ```bash
236
+ node tools/probe-recall.mjs # the six queries the rerank was tuned on
237
+ node tools/probe-recall-heldout.mjs # seven unseen, everyday-worded queries
238
+ node tools/probe-recall-final.mjs # ten queries written before the hybrid change
239
+ ```
240
+
241
+ The three sets exist because one set cannot tell honest progress from
242
+ overfitting. `probe-recall.mjs` holds the queries the coverage rerank was tuned
243
+ against, so it proves only that the code fits its own training set;
244
+ `probe-recall-heldout.mjs` was written afterwards to expose that rerank's blind
245
+ spots, and `probe-recall-final.mjs` was written *before* the hybrid change and
246
+ never used to design it. All three rose when the hybrid landed, which is the
247
+ evidence that matters:
248
+
249
+ | query set | MRR before | MRR after | recall@5 before | recall@5 after |
250
+ |---|---|---|---|---|
251
+ | tuned (6) | 0.833 | **0.917** | — | — |
252
+ | held-out (7) | 0.339 | **0.600** | 0.714 | **0.857** |
253
+ | final (10) | 0.466 | **0.733** | 0.800 | **1.000** |
254
+
255
+ Ranking runs three lexical routes with different blind spots and fuses them by
256
+ rank, following the hybrid-retrieval design in chapter 3 of 《深入理解 AI Agent》:
257
+
258
+ | route | vocabulary | scored by | blind spot it covers |
259
+ |---|---|---|---|
260
+ | precise | character bigrams + ASCII words | BM25 | — |
261
+ | loose | single characters + ASCII words | IDF-weighted coverage | partial word overlap |
262
+ | word | words from a general Chinese dictionary | BM25 | over-matching across word boundaries |
263
+
264
+ The third route exists because character bigrams cannot tell 「车」 inside
265
+ 「车站」 from 「车」 standing alone, so they match across word boundaries; a word
266
+ list fixes exactly that. It is weaker on anything outside the list — personal
267
+ names, code identifiers, new coinages — which is why it joins the fusion rather
268
+ than replacing a route. A third opinion only helps when it is wrong in different
269
+ places, and `tools/ablate-word-route.mjs` is what checks that it is.
270
+
271
+ The book fuses a dense route with a sparse one; recall runs offline and must stay
272
+ offline, so every route here is lexical.
273
+
274
+ That limit is worth stating plainly: no route crosses languages, so a query that
275
+ shares no character with a memory — 「checkpoint」 against 「快照」 — still scores
276
+ zero. The hybrid recovers ranking order among memories that are already
277
+ reachable; it cannot invent a match where no character is shared.
278
+
279
+ ## What actually gets injected
280
+
281
+ `recallLimit` is a ceiling, not a quota. Filling it on every turn is how a weak
282
+ question drags six unrelated memories into the prompt and then asserts they are
283
+ relevant. The injected count steps down with the evidence — the full budget for a
284
+ strong match, fewer for a weaker one, and one entry when the question barely
285
+ matches anything. A question that matches nothing injects nothing.
286
+
287
+ A small resident layer runs alongside: the two most important memories ride along
288
+ regardless of the question. Without it, an ordinary follow-up like 「继续」 shares
289
+ no vocabulary with anything, a strict relevance filter would inject nothing at
290
+ all, and the feature would look broken rather than selective. Set
291
+ `residentLimit: 0` to turn it off.
292
+
293
+ Every candidate that is *not* injected is reported with the rule that dropped it
294
+ (`no-match` or `capacity`). A memory system that silently decides not to recall
295
+ something is indistinguishable from one that has forgotten it; naming the reason
296
+ is the difference between a black box and a debuggable feature.
297
+
298
+ The memory settings page has a **"What would this question recall?"** box that
299
+ shows this live: type a question, see which memories it pulls in, how many the
300
+ evidence justifies, and why each of the rest was left out. The dry run is
301
+ read-only (`touch: false`), so trying queries cannot re-rank your own library.
302
+
303
+ ## Fact keys: the same fact, restated
304
+
305
+ Lexical similarity cannot see a value change. 「拼团还没开」 and 「拼团开了」 are
306
+ the same fact before and after, but they look unrelated; 「身高一米七」 and
307
+ 「长胖到 55 公斤」 look alike but are different facts. Neither comparison can be
308
+ made from the text.
309
+
310
+ So an entry may carry a `subject.aspect` key — `她.身高`, `约定.见面地点`. Entries
311
+ sharing a key are the same fact, and only the newest is recalled; the older ones
312
+ are marked superseded, stay on disk, and are never deleted. Restating a superseded
313
+ fact revives that record rather than forking a second one.
314
+
315
+ Two rules keep it safe. A key that is too broad is **refused** rather than
316
+ trusted — a single-segment key like `身高` names a category, not a fact, so the
317
+ entry simply opts out of supersession. And a key must never contain the value:
318
+ `她.身高170` changes whenever the height does, so the two records never meet. An
319
+ absent key is harmless; a wrong key can retire a good memory.
320
+
321
+ ## If your memory library was affected
322
+
323
+ Between 2026-11-04 and the fix in this version, a self-triggering bug made the
324
+ plugin obey its own instruction text: it scanned every message with
325
+ `role === 'user'` without checking `source.kind`, harness pipeline messages carry
326
+ that role, and this plugin's injected memory block ends with the literal example
327
+ `【记住:…】`. The plugin captured that placeholder and stored it as a memory, 154
328
+ times.
329
+
330
+ Three guards now prevent it, each with a regression test: only user-authored
331
+ messages are obeyed, placeholder-only captures are refused, and identical text
332
+ reinforces instead of appending. Real memories were never overwritten — the write
333
+ path never rewrites text — so they are recoverable from any pre-poisoning
334
+ snapshot, including the atomic write's leftover `.tmp` file:
335
+
336
+ ```bash
337
+ node tools/repair-memory-store.mjs # dry run: reports the plan
338
+ node tools/repair-memory-store.mjs --apply # backs up first, then repairs
339
+ ```
340
+
341
+ The repair backs the current store up under `memory/backups/` before writing
342
+ anything, and it never deletes real content.
343
+
344
+ `docs/pitfalls.md` collects every trap hit while building this bundle, each written
345
+ as symptom → root cause → fix. Read it before changing the snapshot, compaction,
346
+ memory, or export paths.
347
+
348
+ ## Limits
349
+
350
+ * Rewinding does not rewrite the durable log; it forks at the target message and archives the old session. That
351
+ is the only shape the append-only log supports, and it is what makes undo possible at all.
352
+ * **Edit-and-resend appends, it does not replace.** The edited question is sent as a new turn in the same
353
+ session; the original question and its reply stay in the history above. This is a property of the append-only
354
+ log, not a shortcut: a plugin cannot erase events that were already written. Products like Doubao or ChatGPT can
355
+ replace a message in place because they own a mutable message table. If you need the old exchange to actually
356
+ disappear, use rewind (which forks) instead — it has that behaviour, at the cost of a new session.
357
+ * **Tags are normalised, not taken verbatim.** Spellings of the same domain fold together (`DSH` → `dsh`,
358
+ `踩坑` → `错误教训`), and words that name this plugin's own UI (`快照`, `删除`) are dropped rather than stored,
359
+ because a tag nobody can filter by is worse than no tag. New domains are still accepted.
360
+ * Snapshots cover files, not external side effects: database state, running processes, and network calls are out
361
+ of scope.
362
+ * Cross-session memory is this bundle's ledger (`memory/`), not a Host feature; it is exposed through the settings
363
+ page and participates in rollback because the bundle owns it.
364
+ * The HTTP route is protected by DSH Connection (Host/Origin checks plus the browser session), so it is reachable
365
+ only from the authenticated Web GUI.
366
+ * Retrieval is lexical. Neither route crosses languages, so a query sharing no character with a memory
367
+ (`checkpoint` against `快照`) scores zero; the hybrid improves ordering among reachable memories, it cannot
368
+ invent a match.
369
+
370
+ ## Author
371
+
372
+ Built by **heixiua** — <https://github.com/heixiua/dsh-session-suite>.
373
+
374
+ Every source file, the git history, and the published npm tarball carry this
375
+ identity. If you found this plugin through a fork or a repackaged bundle, the
376
+ upstream repository above is the original.
377
+
378
+ ## License
379
+
380
+ MIT. The bundle ships one third-party data file — a Chinese word list derived
381
+ from [jieba](https://github.com/fxsjy/jieba) (MIT) — whose provenance, changes,
382
+ and full licence text are recorded in [THIRD-PARTY.md](THIRD-PARTY.md).
package/README.zh.md ADDED
@@ -0,0 +1,178 @@
1
+ # dsh-session-suite(会话套件)
2
+
3
+ 为 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)提供会话检查点能力:把对话
4
+ **连同工作区文件和会话记忆一起**回滚到任意一条消息,永久删除会话,把会话导出成图片或单文件网页,
5
+ 并且维护一套**跨会话、跨工作区的长期记忆**。
6
+
7
+ 这是一个 DSH 插件包(Host + Web Client),对标 Trae 的会话操作体验,已在 desktop profile 上验证加载。
8
+
9
+ | 能力 | 位置 |
10
+ |---|---|
11
+ | 回滚到某条消息(对话 + 文件 + 记忆) | 每条最终助手消息的 ↺ 按钮,输入框上方的行内确认条 |
12
+ | 删除某一轮(只动对话与记忆) | ↺ 旁边的 🗑 按钮 |
13
+ | 删除选中的一段对话 | 会话 `…` 菜单 → **删除对话片段**(可选轮次) |
14
+ | 永久删除会话 | 会话 `…` 菜单 → **删除任务**(二次确认弹窗) |
15
+ | 分享选中的片段 | 会话 `…` 菜单 → **分享会话**:勾选轮次后生成图片 / 网页 / Markdown / JSON / 复制文本 |
16
+ | 快照管理 | 会话 `…` 菜单 → **会话快照**,以及 设置 → **会话快照** |
17
+ | 长期记忆(全局 + 工作区双库) | 设置 → **长期记忆**;`【记住:…】`;`memory` 工具 |
18
+ | 用 WorkBuddy / Trae 积分跑模型 | **不在本包内** —— 见 [用 WorkBuddy 和 Trae 的积分](docs/credits.zh.md) |
19
+
20
+ ### 长期记忆
21
+
22
+ 两个隔离的库(`global` 与按工作区划分的 `workspace`)、四因子重要性评分(频率 · 新近度 · 情感强度 · 独特性)、
23
+ 动态衰减加冷归档、相似聚类并压缩成摘要、以及从重复标签里提炼规则型记忆。
24
+
25
+ 最关键的一点:**隐式记忆不会偷偷入库**。每轮结束后由一次轻量模型调用提出候选,进入设置页的待确认队列,
26
+ 只有你点「保留」才真正写入;而 `【记住:…】` / `【新增全局记忆:…】` 这类显式指令会跳过队列直接写入。
27
+
28
+ 评分公式、阈值、生命周期、指令、存储结构都在 [docs/long-term-memory.md](docs/long-term-memory.md)。
29
+
30
+
31
+ ### 各操作分别动到什么
32
+
33
+ | 操作 | 对话 | 会话记忆 | 工作区文件 |
34
+ |---|---|---|---|
35
+ | 回滚(↺) | 截断到该消息 | 裁剪到该处 | **恢复到该轮快照** |
36
+ | 删除一轮(🗑) | 提问 + 回答 + 中间的步骤一起移除 | 裁剪到该处 | 不动 |
37
+ | 删除对话片段 | 从选中的第一轮(含它的提问)截断 | 裁剪到该处 | 不动 |
38
+ | 删除任务 | 从磁盘移除 | 移除 | 不动(文件夹保留) |
39
+
40
+ 只有回滚会把文件写回去,其它删除都不碰工作区。会话日志是只追加的,所以「截断」只能是「保留前缀 + 原来的会话退休」——选中范围之后的内容会一起消失。
41
+
42
+ ### 分享
43
+
44
+ 分享弹窗会列出每一轮对话(你的提问 + 回答预览),默认全选,取消勾选即可排除。然后选输出形式:
45
+
46
+ * **图片**:把选中轮次画成一张 PNG(用 canvas 直接画,不依赖任何第三方库);
47
+ * **网页文件**:单文件 HTML,内联样式、跟随明暗主题、可直接打印成 PDF;
48
+ * **Markdown** / **JSON**:贴进 issue 或喂给别的工具;
49
+ * **复制文本**:纯文本进剪贴板。
50
+
51
+ 两种富格式都会真正**渲染 Markdown**,而不是原样吐出:标题、粗体/斜体、行内代码、代码块、有序/无序列表、引用、管道表格、链接。Host 侧把日志解析成带内联样式的 block(`lib/markdown.js`),HTML 渲染器和 canvas 渲染器吃的是同一份结构,所以网页和图片不会出现两种排版。
52
+
53
+ 截图会跟着一起导出:附件不在会话日志里,Host 通过 `attachments.imageHostPath` 取到原文件,按预算内联成 data URL,网页因此仍是单文件。图片里截图按整卡宽度绘制(限高、保持比例);网页里每张截图是一个按钮,点开用内置查看器按原始分辨率看——页面只带一小段内联脚本,不发任何外部请求。
54
+
55
+ 另外要说清楚:**位图(PNG/JPG)里的图片不可能点开**。需要「点开看原图」就分享网页文件,而不是导出的 PNG。
56
+
57
+ ### 导出到底包含什么
58
+
59
+ 会话日志里大部分不是对话:运行环境、技能目录、时间采样、子代理回执、工具调用,以及模型在工具之间写的过渡性说明。这些都不该进导出:
60
+
61
+ * 只有 `source.kind === "user"` 的消息才算「你说了什么」,user 角色下的其它事件全部丢弃;
62
+ * 默认勾选 **只导出最终回答**:每轮保留你的提问 + 最后那条答复;
63
+ * 取消勾选则导出每个助手片段,包括工具调用之间的过程说明;
64
+ * 富格式里永远不出现工具输出,Markdown / JSON 也走同一套取舍。
65
+
66
+ 同一轮里的多个助手片段会合并成一张卡片,只有工具调用、没有正文的步骤直接丢弃,不会导出成一堆空标签。
67
+
68
+ 真正的「在线链接」需要一台服务器托管内容;这个包用可离线打开的网页文件来替代,而不是生成一个只有本机能打开的链接。
69
+
70
+ ## 安装
71
+
72
+ 从 npm 安装:
73
+
74
+ ```bash
75
+ dsh plugin --profile desktop install @heixui/dsh-session-suite
76
+ ```
77
+
78
+ > 发布用的是带作用域的包名,因为不带作用域的 `dsh-session-suite` 在 npm 上属于另一个项目
79
+ > (`chc-1986/dsh-session-suite`),不是这个。作用域只是 npm 上的包身份——插件 id、存储目录、
80
+ > bundle 名仍然是 `dsh-session-suite`,所以旧版本留下的数据照样能找到。
81
+
82
+ 从本仓库检出目录安装:
83
+
84
+ ```bash
85
+ # 从本仓库检出目录安装
86
+ dsh plugin --profile desktop install .
87
+ ```
88
+
89
+ 或者让运行中的 Harness 安装(`plugin_manager` 就是这样做的):
90
+
91
+ ```js
92
+ plugin_manager({ action: 'install_bundle', target: 'D:/path/to/dsh-session-suite' })
93
+ ```
94
+
95
+ 安装后刷新页面即可。整个套件只有一个 Host 行(`session-suite`),配置写在 `cordis.patch.yml`;Client 部分由
96
+ `package.json` 的 `dsh.client` 字段自动发现。
97
+
98
+ ## 回滚是怎么实现的
99
+
100
+ DSH 的会话日志是**只追加**的,而且是唯一事实来源,所以回滚不能删除事件。套件把它做成一次原子的三段式操作:
101
+
102
+ 1. **保护**:先给工作区和记忆账本拍一个临时 *undo* 快照(10 分钟有效)。
103
+ 2. **对话**:`sessionController.fork({ sessionId, atSeq })` 把事件前缀完整分叉成新会话(边界包含该消息、
104
+ 沿用同一个 `cwd` 与 agent preset)。
105
+ 3. **文件**:恢复该轮开始时记录的快照——改过的文件写回、轮次里新建的文件删除、会话开始前就存在的文件不动。
106
+ 4. **记忆**:裁掉晚于目标序列的记忆条目。
107
+ 5. **退休**:把原会话归档(侧边栏隐藏,日志保留),整个过程不销毁任何数据。
108
+
109
+ 「撤销回滚」会把这五项全部还原:文件、记忆、被归档的会话,以及分叉出来的子会话。
110
+
111
+ ## 快照在磁盘上
112
+
113
+ ```
114
+ %DSH_HOME%/storages/dsh-session-suite/
115
+ sessions/<sessionId>/index.json # 检查点索引
116
+ sessions/<sessionId>/manifests/*.json # 路径 -> {hash, size, mtime}
117
+ sessions/<sessionId>/blobs/<ab>/<hash> # gzip 压缩的文件内容,按内容寻址
118
+ memory/<sessionId>.json # 会话记忆账本
119
+ memory/_global.json # 跨会话长期记忆
120
+ undo/<id>.json # 回滚撤销包
121
+ ```
122
+
123
+ * **增量**:manifest 只记录路径与哈希,相同内容只存一份;未变化的文件靠上一份 manifest 的
124
+ `(size, mtime)` 指纹直接跳过,不再读取。
125
+ * **配额**:每个会话保留最近 10 个自动检查点;锁定的一律保留;临时撤销包不占配额,10 分钟后清理。
126
+ * **忽略清单**:`node_modules`、`.git`、缓存、构建产物、日志、锁文件、二进制文件都不记录;`config.ignore`
127
+ 可追加自定义规则。
128
+ * **上限**:单文件超过 2 MiB、单次超过 4000 个文件会跳过,避免拖慢对话。
129
+
130
+ ## 配置
131
+
132
+ ```yaml
133
+ - insert:
134
+ - id: session-suite
135
+ name: dsh-session-suite
136
+ config:
137
+ keep: 10 # 每个会话保留的自动检查点数量
138
+ autoCheckpoint: true # 每轮开始时自动拍快照
139
+ maxFileBytes: 2097152
140
+ maxFiles: 4000
141
+ sweepIntervalMs: 300000
142
+ ignore: [] # 追加的忽略规则
143
+ ```
144
+
145
+ ## 目录结构
146
+
147
+ ```
148
+ index.js Host 半部:注册路由、按轮拍快照、清理定时器
149
+ client.js Client 半部:菜单行、消息操作、确认条、弹窗、设置页
150
+ lib/api.js 受保护的 Fetch 路由(POST api/session-suite)
151
+ lib/service.js 会话操作:rollback / undo / delete / share / deleteMessage
152
+ lib/checkpoints.js 内容寻址的增量快照存储
153
+ lib/memory.js 会话记忆账本(支持按序列裁剪)
154
+ lib/share.js 会话日志 → Markdown / 文本 / JSON
155
+ locale/{en,zh}.json 插件管理器的展示信息与文案
156
+ tools/ 自测脚本与预清理脚本(不随插件打包)
157
+ docs/ 需求与 API 笔记
158
+ ```
159
+
160
+ ## 测试
161
+
162
+ 两个自测脚本都在临时目录里跑,不会碰真实的 DSH 数据。
163
+
164
+ ```bash
165
+ node tools/selftest.mjs # 快照 / 忽略规则 / 恢复 / 淘汰 / 记忆
166
+ node tools/selftest-rollback.mjs # 回滚 / 撤销 / 删除消息 / 分享 / 永久删除
167
+ ```
168
+
169
+ ## 已知边界
170
+
171
+ * 回滚不改写日志,而是「分叉到目标消息 + 归档原会话」。这是只追加日志支持的唯一形态,也正是撤销能够成立的原因。
172
+ * 快照覆盖的是文件,不覆盖外部副作用:数据库状态、运行中的进程、网络调用都不在其中。
173
+ * 跨会话记忆是本套件自己的账本(`memory/`),不是 DSH 内核能力;因为由套件拥有,所以能一起回滚。
174
+ * HTTP 路由受 DSH Connection 保护(Host/Origin 校验 + 浏览器会话),只有已鉴权的 Web GUI 能访问。
175
+
176
+ ## 许可证
177
+
178
+ MIT。
package/THIRD-PARTY.md ADDED
@@ -0,0 +1,64 @@
1
+ # Third-party material
2
+
3
+ `dsh-session-suite` itself is MIT licensed (see [LICENSE](LICENSE)). It bundles
4
+ one third-party data file, listed here with its licence and provenance.
5
+
6
+ ## `lib/data/jieba-dict.txt`
7
+
8
+ A Chinese word list used for word-boundary segmentation on the retrieval path. It
9
+ is derived from **jieba**'s `extra_dict/dict.txt.small`.
10
+
11
+ - Upstream: <https://github.com/fxsjy/jieba> (`extra_dict/dict.txt.small`)
12
+ - Licence: MIT — Copyright (c) 2013 Sun Junyi
13
+ - What was changed: the frequency and part-of-speech columns were dropped, so the
14
+ file holds one word per line; duplicate entries were removed. No words were
15
+ added, removed, or edited.
16
+ - Size: 109,750 entries, ~920 KB
17
+ - Regenerate with: `node tools/fetch-jieba-dict.mjs` (add `--force` to refetch)
18
+
19
+ The file is a lookup table loaded once per process. It is never sent to a model
20
+ and adds nothing to prompt size or per-turn token cost.
21
+
22
+ ### jieba's MIT licence
23
+
24
+ ```
25
+ The MIT License (MIT)
26
+
27
+ Copyright (c) 2013 Sun Junyi
28
+
29
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
30
+ this software and associated documentation files (the "Software"), to deal in
31
+ the Software without restriction, including without limitation the rights to
32
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
33
+ the Software, and to permit persons to whom the Software is furnished to do so,
34
+ subject to the following conditions:
35
+
36
+ The above copyright notice and this permission notice shall be included in all
37
+ copies or substantial portions of the Software.
38
+
39
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
40
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
41
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
42
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
43
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
44
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
45
+ ```
46
+
47
+ ## Design influences, not code
48
+
49
+ Two third-party DSH plugins were read while designing the long-term memory
50
+ features. **No code was copied from either**, and neither is a dependency.
51
+
52
+ - [`dsh-fish-memory`](https://github.com/Yuuyuko-uu/dsh-fish-memory) — its licence
53
+ is a custom, non-OSI "Yuuyuko-uu 插件许可 1.0" that permits use and
54
+ modification but requires attribution on redistribution, so it is source for
55
+ ideas only. The resident-memory-layer idea (durable facts get their own small
56
+ quota instead of competing with question-matched entries) and the practice of
57
+ telling the user *why* a memory was not recalled both come from reading it.
58
+ The word-list segmentation approach it demonstrated is what motivated adopting
59
+ jieba's MIT word list here.
60
+ - [`dsh-context`](https://github.com/bowenliang123/dsh-context) — Apache-2.0.
61
+ Read to confirm the two plugins do not collide on any slot, tool, or route.
62
+
63
+ Coinage of the word list, the retrieval fusion, and every line of this bundle
64
+ are original to this project.