@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 +21 -0
- package/README.md +381 -2
- package/README.zh.md +178 -0
- package/THIRD-PARTY.md +64 -0
- package/client.js +2974 -0
- package/cordis.patch.yml +77 -0
- package/icon.svg +19 -0
- package/index.js +211 -0
- package/lib/api.js +387 -0
- package/lib/checkpoints.js +774 -0
- package/lib/data/jieba-dict.txt +109750 -0
- package/lib/factkeys.js +212 -0
- package/lib/markdown.js +252 -0
- package/lib/memory-engine.js +1323 -0
- package/lib/memory-host.js +683 -0
- package/lib/memory.js +224 -0
- package/lib/retrieval.js +217 -0
- package/lib/service.js +1273 -0
- package/lib/share.js +641 -0
- package/lib/step-tag-filter.js +199 -0
- package/lib/tags.js +140 -0
- package/lib/util.js +261 -0
- package/lib/wordlist.js +183 -0
- package/locale/en.json +87 -0
- package/locale/zh.json +87 -0
- package/package.json +76 -4
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
|
-
#
|
|
1
|
+
# dsh-session-suite
|
|
2
2
|
|
|
3
|
-
|
|
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.
|