dsh-session-manager 0.4.6 → 0.4.7
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/CHANGELOG.md +131 -119
- package/README.md +106 -112
- package/README.zh.md +27 -35
- package/lib/client.js +20 -16
- package/lib/compat/dsh-adapter.js +200 -0
- package/lib/compat/zstd-frames.js +93 -0
- package/lib/index.js +45 -127
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,119 +1,131 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.4.
|
|
4
|
-
|
|
5
|
-
- **fix(
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
- **fix(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
- **
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
- **
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
- **
|
|
84
|
-
preset
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.4.7 — 2026-09-10
|
|
4
|
+
|
|
5
|
+
- **fix(disk scan)**: include `session.v3.jsonl.zstd` in the on-disk
|
|
6
|
+
filename list used by readSessionArtifact() and listSessionHeaders().
|
|
7
|
+
DSH 0.1.5-rc.1's persistence backend writes generation v3 artifacts at
|
|
8
|
+
that filename; the 0.4.6 release scanned only v2/plaintext names, so
|
|
9
|
+
fresh sessions appeared to have no disk record and the move/migrate
|
|
10
|
+
endpoints failed with "会话没有磁盘记录" / "session has no artifact".
|
|
11
|
+
|
|
12
|
+
- **chore**: bump version to 0.4.7.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
## 0.4.6 — 2026-09-05
|
|
16
|
+
|
|
17
|
+
- **fix(persistence)**: read all concatenated Zstandard frames in a session
|
|
18
|
+
artifact. DSH writes one frame for the header and additional frames for
|
|
19
|
+
event batches; reading only the first frame made sessions appear to lose
|
|
20
|
+
their event history after refresh.
|
|
21
|
+
- **fix(move)**: preserve the session generation/header version when
|
|
22
|
+
re-homing an artifact, and synchronize the live session, persistence
|
|
23
|
+
coordinator, registry indexes, and workspace accounting after a move.
|
|
24
|
+
This avoids stale-path `ENOENT` failures and v2/v0 filename/header
|
|
25
|
+
mismatches on the next DSH startup.
|
|
26
|
+
- **fix(preset migration)**: fall back to an id-based artifact scan when a
|
|
27
|
+
persistence locate result points at a stale path, and mirror cold-path
|
|
28
|
+
migrations into the live session projection.
|
|
29
|
+
- **fix(client)**: stop calling the removed/unavailable
|
|
30
|
+
`noteAgentPreset` client method; refresh the session projection instead.
|
|
31
|
+
- **recovery**: add `scripts/heal-v2-sessions.ps1` for the DSH 0.1.1-rc.2
|
|
32
|
+
boot-time quirk where `session.v2.jsonl.zstd` contains a `version: 0`
|
|
33
|
+
header. Run it before starting DSH; this repair must happen before DSH's
|
|
34
|
+
workspace initialization, which is earlier than user plugin `apply()`.
|
|
35
|
+
|
|
36
|
+
- **docs**: bump version to 0.4.6.
|
|
37
|
+
## 0.4.5 — 2026-09-05
|
|
38
|
+
|
|
39
|
+
- **fix(move)**: keep the live session and agent in place during a cross-workspace
|
|
40
|
+
move. The previous code tore down the live agent/session, moved the file, then
|
|
41
|
+
called `ctx.agents.resume` to re-create the agent. DSH's `agent/status` event
|
|
42
|
+
is only emitted on phase changes, so a freshly resumed agent never told the
|
|
43
|
+
client it was now idle, leaving the sidebar's model selector and send button
|
|
44
|
+
disabled ("会话不可用") until a manual browser refresh. The new path flushes
|
|
45
|
+
pending events to disk, updates the in-memory session header + coordinator
|
|
46
|
+
state + workspace accounting in place, and atomically renames the artifact,
|
|
47
|
+
so the agent's UI keeps showing the same in-memory session with no client
|
|
48
|
+
re-init.
|
|
49
|
+
- **fix(preset migration)**: drop the over-strict `persistence.readRaw` /
|
|
50
|
+
`persistence.list` precondition that caused the
|
|
51
|
+
`/session-manager/api/preset-scan` endpoint to fail with
|
|
52
|
+
`current persistence backend does not support readRaw/list` on a default DSH
|
|
53
|
+
build. The actual `JsonlSessionPersistence` backend exposes both methods, so
|
|
54
|
+
the precondition is replaced with a try/catch around `listSessionHeaders` that
|
|
55
|
+
converts any missing-method failure into a useful
|
|
56
|
+
`failed to enumerate sessions: <detail>` message.
|
|
57
|
+
- **chore**: remove the now-unused `quietLive` / `releaseLiveSession` helpers
|
|
58
|
+
and squash the per-route indentation noise around `/move` and `/workspaces`.
|
|
59
|
+
|
|
60
|
+
## 0.4.4 — 2026-09-03
|
|
61
|
+
|
|
62
|
+
- **docs**: rename the English README wording from `conversation` to `session` to align with the plugin name (`dsh-session-manager`), the Chinese README (`会话`), the DSH host APIs, and the [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) registry entry.
|
|
63
|
+
- **chore**: rewrite `package.json` `description` to use `Session manager` / `sessions` for the same alignment, and bump the version to `0.4.4`.
|
|
64
|
+
- **chore(repo)**: update the GitHub repository description to match.
|
|
65
|
+
|
|
66
|
+
## 0.4.3 — 2026-09-03
|
|
67
|
+
|
|
68
|
+
- **docs**: add the [Awesome DSH Plugin](https://awesome-dsh-plugin.com) badge to `README.md` / `README.zh.md` so the repo surfaces its curated registry membership.
|
|
69
|
+
|
|
70
|
+
## 0.4.2 — 2026-09-03
|
|
71
|
+
|
|
72
|
+
- **feat(theme)**: dialogs now auto-follow DSH''s dark/light theme (`data-ds-dark-theme` / ` `code-scheme` / `data-theme`) via a single `data-sm-theme` attribute and scoped CSS variables, with inline `background` / `color` / `border-color` applied to each dialog root so they stay opaque regardless of how DSH resolves its own tokens. The previous manual light/dark toggle button is removed.
|
|
73
|
+
- **docs**: aligned bilingual README structure, dropped the obsolete "no bulk migration" wording, and added an Acknowledgments section that thanks the users and the contributors filing issues and opening PRs. Listed [dsh-market](https://github.com/dsh-market/dsh-market) and [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) as install sources.
|
|
74
|
+
- **chore**: rewrote `package.json` `description` to match the new English summary.
|
|
75
|
+
|
|
76
|
+
## 0.4.1 — 2026-08-28
|
|
77
|
+
|
|
78
|
+
- **fix(ui)**: keep the title-bar **Delete conversation** button readable on hover with a red background, white text, and red border; remove the unused legacy danger-button rules.
|
|
79
|
+
|
|
80
|
+
## 0.4.0 — 2026-08-28
|
|
81
|
+
|
|
82
|
+
- **feat(session preset migration)**: replaces the former bulk workflow with a
|
|
83
|
+
per-conversation **Migrate preset** action in Session manager. It resolves the
|
|
84
|
+
effective preset from the latest `agent-preset/selected` event or, when absent,
|
|
85
|
+
the session header, then safely updates that one conversation.
|
|
86
|
+
- **fix(lifecycle)**: moving a conversation or migrating its preset now retires stale
|
|
87
|
+
live agents and persistence owners before refresh. This prevents resume failures
|
|
88
|
+
such as `already has a live persistence owner`.
|
|
89
|
+
- **fix(move)**: refreshes session and workspace state immediately and once more after
|
|
90
|
+
the host event race, so a moved conversation reappears in its target workspace
|
|
91
|
+
without a manual browser refresh.
|
|
92
|
+
- **ui**: finalizes header actions and Session manager dialogs: red delete actions,
|
|
93
|
+
per-row preset migration, consistent dialog placement, readable hover states, and
|
|
94
|
+
a close button beside the manager title.
|
|
95
|
+
- **docs**: refreshes bilingual documentation and npm metadata for the single-session
|
|
96
|
+
preset migration workflow.
|
|
97
|
+
|
|
98
|
+
## 0.3.0 — 2026-08-27
|
|
99
|
+
|
|
100
|
+
- **feat(preset migration)**: introduced preset migration support for conversations
|
|
101
|
+
whose configured Agent preset was renamed or removed.
|
|
102
|
+
- **feat(move)**: added workspace move handling and client-side workspace refreshes.
|
|
103
|
+
|
|
104
|
+
## 0.2.1 — 2026-08-26
|
|
105
|
+
|
|
106
|
+
- **fix(move)**: reimplemented cross-workspace moves so the session artifact, stored
|
|
107
|
+
`cwd`, and workspace accounting are moved together while preserving history,
|
|
108
|
+
title, archive state, and derived-session relationships.
|
|
109
|
+
- **feat(workspaces API)**: added the workspace projection endpoint used by the move UI.
|
|
110
|
+
- **guard**: reject subagent and transient blank-session placeholders for move actions.
|
|
111
|
+
|
|
112
|
+
## 0.2.0 — 2026-08-16
|
|
113
|
+
|
|
114
|
+
> ⚠️ The initial workspace-move implementation was superseded by 0.2.1.
|
|
115
|
+
|
|
116
|
+
- **feat(move)**: added the initial move-to-workspace UI and host endpoints.
|
|
117
|
+
|
|
118
|
+
## 0.1.2 — 2026-08-16
|
|
119
|
+
|
|
120
|
+
- **docs**: synchronized package metadata and bilingual README files for publication.
|
|
121
|
+
|
|
122
|
+
## 0.1.1 — 2026-08-16
|
|
123
|
+
|
|
124
|
+
- **fix(panel)**: hide transient blank-session placeholders from the manager panel.
|
|
125
|
+
- **test**: added the host API smoke test.
|
|
126
|
+
- **ci**: added syntax and package-content verification.
|
|
127
|
+
|
|
128
|
+
## 0.1.0 — 2026-08-16
|
|
129
|
+
|
|
130
|
+
- **feat**: initial session deletion with confirmation, archive management, and the
|
|
131
|
+
Session manager panel.
|
package/README.md
CHANGED
|
@@ -1,113 +1,107 @@
|
|
|
1
|
-
# dsh-session-manager — session manager for DeepSeek Harness
|
|
2
|
-
|
|
3
|
-
English | [中文](README.zh.md)
|
|
4
|
-
|
|
5
|
-
[](https://www.npmjs.com/package/dsh-session-manager)
|
|
6
|
-
[](https://github.com/hkkz9522/dsh-session-manager)
|
|
7
|
-
[](https://github.com/hkkz9522/dsh-session-manager/actions/workflows/ci.yml)
|
|
8
|
-
[](https://awesome-dsh-plugin.com)
|
|
9
|
-
|
|
10
|
-
A DeepSeek Harness (DSH) Web plugin for session management: delete sessions, archive sessions, move sessions across workspaces, and migrate a session's Agent preset. Suggestions are welcome on GitHub.
|
|
11
|
-
|
|
12
|
-
## Features
|
|
13
|
-
|
|
14
|
-
- **Archive / unarchive** sessions.
|
|
15
|
-
- **Delete sessions** with an explicit irreversible-action confirmation.
|
|
16
|
-
- **Move to workspace**: preserves history, title, archive state, and derived-session relationships, and
|
|
17
|
-
- **Migrate Agent preset**: change the preset on demand. Typical use case: when the original preset was renamed or removed and the session can no longer resume, you can repair that session.
|
|
18
|
-
- **Session manager**: browse active and archived sessions in the sidebar, and run Open, Archive / Unarchive, Move,
|
|
19
|
-
- The current session's title area offers Archive / Unarchive, Move to workspace, and a red Delete session button.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
- **
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
## Acknowledgments
|
|
108
|
-
|
|
109
|
-
Thanks to everyone who installs and uses dsh-session-manager, and to the people who file issues and open pull requests to help improve it. This plugin is listed in [dsh-market](https://github.com/dsh-market/dsh-market) and [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin). Suggestions and feedback are welcome.
|
|
110
|
-
|
|
111
|
-
## License
|
|
112
|
-
|
|
1
|
+
# dsh-session-manager — session manager for DeepSeek Harness
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/dsh-session-manager)
|
|
6
|
+
[](https://github.com/hkkz9522/dsh-session-manager)
|
|
7
|
+
[](https://github.com/hkkz9522/dsh-session-manager/actions/workflows/ci.yml)
|
|
8
|
+
[](https://awesome-dsh-plugin.com)
|
|
9
|
+
|
|
10
|
+
A DeepSeek Harness (DSH) Web plugin for session management: delete sessions, archive sessions, move sessions across workspaces, and migrate a session's Agent preset. Suggestions are welcome on GitHub.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- **Archive / unarchive** sessions.
|
|
15
|
+
- **Delete sessions** with an explicit irreversible-action confirmation.
|
|
16
|
+
- **Move to workspace**: preserves history, title, archive state, and derived-session relationships, and rewrites the session's working directory to the target workspace.
|
|
17
|
+
- **Migrate Agent preset**: change the preset on demand. Typical use case: when the original preset was renamed or removed and the session can no longer resume, you can repair that session.
|
|
18
|
+
- **Session manager panel**: browse active and archived sessions in the sidebar, and run Open, Archive / Unarchive, Move, Migrate preset, or Delete on each row.
|
|
19
|
+
- The current session's title area offers Archive / Unarchive, Move to workspace, and a red Delete session button.
|
|
20
|
+
- Each dialog button (Move, Migrate preset, Delete, plus the Session manager toggle) closes its own popup when clicked a second time, matching the built-in title-area buttons.
|
|
21
|
+
|
|
22
|
+
## Where to find the UI
|
|
23
|
+
|
|
24
|
+
- **Session title area (right side):** Archive / Unarchive, Move to workspace, Delete session.
|
|
25
|
+
- **Sidebar footer → Session manager:** browse all sessions (including archived ones) and operate on each one.
|
|
26
|
+
|
|
27
|
+
## Agent preset migration
|
|
28
|
+
|
|
29
|
+
Use this when a session can no longer resume because its original preset no longer exists, for example after removing a custom preset such as `router-standard`.
|
|
30
|
+
|
|
31
|
+
1. Open **Session manager**.
|
|
32
|
+
2. Locate the session and select **Migrate preset**.
|
|
33
|
+
3. Choose one of the currently available target presets and confirm.
|
|
34
|
+
|
|
35
|
+
The plugin determines the session's effective preset from its latest `agent-preset/selected` event when present; otherwise it uses the session header. It then rewrites that event in place (or appends a fresh one if the session has never recorded a selection), so the migration is durable and the prior entry remains visible in the event log as history. For a live session, the new event is appended in memory via `Session.append()` and flushed to disk via `SessionStore.flush()`; the api-gateway's chat panel sees the new preset on the next event fold.
|
|
36
|
+
|
|
37
|
+
> A preset migration changes session metadata only. It does not alter message history, files, or the selected workspace.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
The plugin is listed in [dsh-market](https://github.com/dsh-market/dsh-market) and [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin), and can be installed directly from the **Plugin Marketplace** inside DSH.
|
|
42
|
+
|
|
43
|
+
### From dsh-market
|
|
44
|
+
|
|
45
|
+
```powershell
|
|
46
|
+
dsh plugin --profile web add npm:dsh-session-manager
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### From GitHub
|
|
50
|
+
|
|
51
|
+
```powershell
|
|
52
|
+
dsh plugin --profile web add github:hkkz9522/dsh-session-manager
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Restart DSH Web after installation. If the browser still holds an older client bundle, force refresh with `Ctrl+Shift+R`.
|
|
56
|
+
|
|
57
|
+
### Local development / runtime injection
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
dev_inject_plugin {"dir": "<absolute path to this repository>"}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Safety and behavior
|
|
64
|
+
|
|
65
|
+
- **Deletion is permanent**, so the UI always asks for confirmation.
|
|
66
|
+
- Move and Migrate preset do **not** tear down the live agent or session. They keep the in-memory session/agent alive, write the new artifact in place, update the in-memory session header to point at the new cwd (move) or append the new event (migrate), and refresh the workspace registry. The api-gateway's chat panel therefore stays "available" without a manual refresh.
|
|
67
|
+
- Move rewrites the session's stored `cwd`; subsequent tool calls run in the target workspace.
|
|
68
|
+
- Subagent sessions and transient blank-session placeholders are excluded from Delete, Move, and Migrate preset.
|
|
69
|
+
- The move path encodes the artifact in the backend's own physical layout (zstd frames with a one-header-line first frame, or plain JSONL), matching DSH's own writer. The migrate path rewrites the relevant event in place at the existing file.
|
|
70
|
+
|
|
71
|
+
## Compatibility
|
|
72
|
+
|
|
73
|
+
| Plugin version | Verified DSH version |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| 0.4.7 | v0.1.5-rc.1 |
|
|
76
|
+
| 0.4.4 | 0.1.3-alpha.2 |
|
|
77
|
+
| 0.4.1 | 0.1.3-alpha.2 |
|
|
78
|
+
| 0.4.0 | v0.1.2-rc.1 |
|
|
79
|
+
| 0.1.2 | v0.1.0-rc.7 |
|
|
80
|
+
| 0.1.1 | v0.1.0-rc.7 |
|
|
81
|
+
| 0.1.0 | v0.1.0-rc.7 |
|
|
82
|
+
|
|
83
|
+
The plugin is a Cordis plugin and declares `cordis: ">=4.0.0-rc <5"` as its peer dependency.
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
- `lib/index.js` is the host-side ESM plugin; `lib/client.js` is the Web client bundle. No build step is required.
|
|
88
|
+
- Before submitting changes, run:
|
|
89
|
+
|
|
90
|
+
```powershell
|
|
91
|
+
node --check lib/client.js
|
|
92
|
+
node --check lib/index.js
|
|
93
|
+
node --test test/*.test.mjs
|
|
94
|
+
node scripts/smoke-test.mjs
|
|
95
|
+
git diff --check
|
|
96
|
+
npm pack --dry-run
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Release history is in [CHANGELOG.md](CHANGELOG.md).
|
|
100
|
+
|
|
101
|
+
## Acknowledgments
|
|
102
|
+
|
|
103
|
+
Thanks to everyone who installs and uses dsh-session-manager, and to the people who file issues and open pull requests to help improve it. This plugin is listed in [dsh-market](https://github.com/dsh-market/dsh-market) and [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin). Suggestions and feedback are welcome.
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
113
107
|
[MIT](LICENSE)
|
package/README.zh.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/dsh-session-manager)
|
|
6
6
|
[](https://github.com/hkkz9522/dsh-session-manager)
|
|
7
|
-
[](https://github.com/hkkz9522/dsh-session-manager/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/hkkz9522/dsh-session-manager/actions/workflows/ci.yml)
|
|
8
8
|
[](https://awesome-dsh-plugin.com)
|
|
9
9
|
|
|
10
10
|
用于在 DeepSeek Harness(DSH)Web 中进行会话管理,包括:删除会话、归档会话、跨工作区移动会话、迁移会话的 Agent 预设。欢迎至 GitHub 提意见。
|
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
|
|
14
14
|
- **归档 / 移出归档**会话。
|
|
15
15
|
- **删除会话**:带不可逆操作的二次确认。
|
|
16
|
-
-
|
|
17
|
-
- **迁移 Agent
|
|
18
|
-
- **会话管理窗口**:在侧边栏中浏览未归档和已归档会话,并对每一行执行打开、归档 /
|
|
16
|
+
- **移动至工作区**:保留历史、标题、归档状态和派生会话关系,同时把会话的 `cwd` 更新为目标工作区。
|
|
17
|
+
- **迁移 Agent 预设**:按需修改。典型工况:当原预设被改名或删除,导致会话无法恢复时,可修复该会话。
|
|
18
|
+
- **会话管理窗口**:在侧边栏中浏览未归档和已归档会话,并对每一行执行打开、归档 / 移出归档、移动、迁移预设、删除。
|
|
19
19
|
- 当前会话标题区域提供归档 / 移出归档、移动至工作区和红色的删除会话按钮。
|
|
20
|
+
- 弹窗按钮(移动、迁移预设、删除,以及"会话管理"入口)再次点击会关闭对应弹窗,与标题栏原生按钮行为一致。
|
|
20
21
|
|
|
21
22
|
## UI 入口
|
|
22
23
|
|
|
@@ -31,7 +32,7 @@
|
|
|
31
32
|
|
|
32
33
|
例如:当会话无法恢复,报错表明原 Agent 预设不存在时(例如删掉了 `router-standard`),可以使用迁移功能。
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
插件会读取最后一条 `agent-preset/selected` 事件中的有效预设(若不存在则读取会话 header),然后就地重写该事件(若会话从未记录过选择事件则追加新事件)——这一做法是持久的,旧事件保留在日志中作为历史。对 live session,新事件通过 `Session.append()` 追加到内存,再通过 `SessionStore.flush()` 刷到磁盘;api-gateway 的聊天面板在下次事件折叠时即可看到新预设。
|
|
35
36
|
|
|
36
37
|
> 迁移预设只会修改会话元数据,不会改写历史消息、文件或当前工作区。
|
|
37
38
|
|
|
@@ -59,46 +60,39 @@ dsh plugin --profile web add github:hkkz9522/dsh-session-manager
|
|
|
59
60
|
dev_inject_plugin {"dir": "<本仓库的绝对路径>"}
|
|
60
61
|
```
|
|
61
62
|
|
|
62
|
-
## HTTP API
|
|
63
|
-
|
|
64
|
-
以下本地接口供 Web UI 使用,也可用于集成和排查:
|
|
65
|
-
|
|
66
|
-
```text
|
|
67
|
-
POST /session-manager/api/delete { sessionId }
|
|
68
|
-
POST /session-manager/api/unarchive { sessionId }
|
|
69
|
-
GET /session-manager/api/workspaces
|
|
70
|
-
POST /session-manager/api/move { sessionId, targetWorkspaceId }
|
|
71
|
-
GET /session-manager/api/preset-scan?sessionId=<sessionId>
|
|
72
|
-
POST /session-manager/api/preset-migrate { sessionId, toPreset }
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
示例:将会话迁移到 `standard` 预设。
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
curl -s -X POST http://127.0.0.1:3080/session-manager/api/preset-migrate \
|
|
79
|
-
-H 'content-type: application/json' \
|
|
80
|
-
-d '{"sessionId":"session-...","toPreset":"standard"}'
|
|
81
|
-
```
|
|
82
|
-
|
|
83
63
|
## 安全与行为说明
|
|
84
64
|
|
|
85
65
|
- **删除不可恢复**,因此界面始终要求确认。
|
|
86
|
-
-
|
|
66
|
+
- 移动和迁移预设会先 quiesce 该会话的 live agent(取消运行 + 释放 scope + 移出 SessionStore),即使聊天标签页还开着也能成功;侧边栏会自动刷新。
|
|
87
67
|
- 移动会改写会话保存的 `cwd`,之后的工具调用将在目标工作区执行。
|
|
88
|
-
- subagent
|
|
89
|
-
-
|
|
68
|
+
- subagent 会话和临时空白会话占位不会参与删除、移动或迁移预设。
|
|
69
|
+
- 持久化改写走 DSH 自身的 `open/create/append/flush` 接口,编解码链在内部处理 v2 → v3 格式迁移,写出的工件对当前 DSH 版本始终合法。
|
|
70
|
+
|
|
71
|
+
## 兼容性
|
|
72
|
+
|
|
73
|
+
| 插件版本 | 已验证 DSH 版本 |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| 0.4.7 | v0.1.5-rc.1 |
|
|
76
|
+
| 0.4.4 | 0.1.3-alpha.2 |
|
|
77
|
+
| 0.4.1 | 0.1.3-alpha.2 |
|
|
78
|
+
| 0.4.0 | v0.1.2-rc.1 |
|
|
79
|
+
| 0.1.2 | v0.1.0-rc.7 |
|
|
80
|
+
| 0.1.1 | v0.1.0-rc.7 |
|
|
81
|
+
| 0.1.0 | v0.1.0-rc.7 |
|
|
82
|
+
|
|
83
|
+
本插件是 Cordis 插件,peer dependency 为 `cordis: ">=4.0.0-rc <5"`。
|
|
90
84
|
|
|
91
|
-
##
|
|
85
|
+
## 开发
|
|
92
86
|
|
|
93
|
-
- 本插件是 Cordis 插件,声明的 peer dependency 为 `cordis >=4.0.0-rc <5`。
|
|
94
87
|
- `lib/index.js` 是 host 端 ESM 插件,`lib/client.js` 是 Web 客户端 bundle,无需构建步骤。
|
|
95
88
|
- 提交修改前请运行:
|
|
96
89
|
|
|
97
90
|
```powershell
|
|
98
91
|
node --check lib/client.js
|
|
99
92
|
node --check lib/index.js
|
|
100
|
-
|
|
93
|
+
node --test test/*.test.mjs
|
|
101
94
|
node scripts/smoke-test.mjs
|
|
95
|
+
git diff --check
|
|
102
96
|
npm pack --dry-run
|
|
103
97
|
```
|
|
104
98
|
|
|
@@ -108,10 +102,8 @@ npm pack --dry-run
|
|
|
108
102
|
|
|
109
103
|
感谢每一位安装和使用 dsh-session-manager 的用户,也感谢提交 Issue 与 Pull Request 帮助改进本插件的朋友们。
|
|
110
104
|
|
|
111
|
-
本插件已被 [dsh-market](https://github.com/dsh-market/dsh-market) 和 [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
|
|
112
|
-
欢迎提出修改意见。
|
|
105
|
+
本插件已被 [dsh-market](https://github.com/dsh-market/dsh-market) 和 [awesome-dsh-plugin](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin) 收录。欢迎提出修改意见。
|
|
113
106
|
|
|
114
107
|
## 开源许可
|
|
115
108
|
|
|
116
109
|
[MIT](LICENSE)
|
|
117
|
-
|
package/lib/client.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @dsh-session-manager — Client half.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* Renders the title-bar action cluster (Archive / Unarchive, Move to
|
|
5
|
+
* workspace, Delete session) and the sidebar-foot Session manager panel
|
|
6
|
+
* (Open / Archive / Unarchive / Move / Migrate preset / Delete per row,
|
|
7
|
+
* plus the panel's own filter pills and toggle-close behaviour). No build
|
|
8
|
+
* step: DSH loads the file as a Cordis client bundle via its inject
|
|
9
|
+
* declaration in package.json.
|
|
8
10
|
*/
|
|
9
11
|
window.__ModuleLoader__.load({
|
|
10
12
|
id: "dsh-session-manager",
|
|
@@ -617,6 +619,7 @@ window.__ModuleLoader__.load({
|
|
|
617
619
|
getSnapshot() { return this.open; },
|
|
618
620
|
subscribe(cb) { this.listeners.add(cb); return () => this.listeners.delete(cb); },
|
|
619
621
|
set(v) { if (this.open === v) return; this.open = v; for (const fn of [...this.listeners]) fn(); },
|
|
622
|
+
toggle() { this.open = !this.open; for (const fn of [...this.listeners]) fn(); },
|
|
620
623
|
// Pending action queued by the title-bar buttons (e.g. 移动至工作区).
|
|
621
624
|
// The SessionManagerPanel consumes this on mount so it can pre-open
|
|
622
625
|
// the matching inner dialog (move / delete / migrate) next to itself.
|
|
@@ -727,9 +730,9 @@ window.__ModuleLoader__.load({
|
|
|
727
730
|
h("div", { className: "sm-rowActions" },
|
|
728
731
|
h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => onOpen(s.id) }, t("row.open")),
|
|
729
732
|
h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => runRow(s.id, () => isArchived ? onUnarchive(s.id) : onArchive(s.id)) }, t(isArchived ? "row.unarchive" : "row.archive")),
|
|
730
|
-
h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => setMoveFor(s) }, t("row.move")),
|
|
731
|
-
h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => setMigrateFor(s) }, t("row.migrate")),
|
|
732
|
-
h("button", { type: "button", className: "sm-rowBtn sm-rowBtnDanger", disabled: busy, onClick: () => setConfirmFor(s) }, t("row.delete"))
|
|
733
|
+
h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => { if (moveFor && moveFor.id === s.id) setMoveFor(null); else setMoveFor(s); } }, t("row.move")),
|
|
734
|
+
h("button", { type: "button", className: "sm-rowBtn", disabled: busy, onClick: () => { if (migrateFor && migrateFor.id === s.id) setMigrateFor(null); else setMigrateFor(s); } }, t("row.migrate")),
|
|
735
|
+
h("button", { type: "button", className: "sm-rowBtn sm-rowBtnDanger", disabled: busy, onClick: () => { if (confirmFor && confirmFor.id === s.id) setConfirmFor(null); else setConfirmFor(s); } }, t("row.delete"))
|
|
733
736
|
)
|
|
734
737
|
);
|
|
735
738
|
};
|
|
@@ -1062,10 +1065,11 @@ window.__ModuleLoader__.load({
|
|
|
1062
1065
|
ctx.sessions.open(sessionId);
|
|
1063
1066
|
}
|
|
1064
1067
|
};
|
|
1065
|
-
//
|
|
1066
|
-
//
|
|
1067
|
-
//
|
|
1068
|
-
//
|
|
1068
|
+
// The 0.4.6+ move path keeps the live agent/session intact (no
|
|
1069
|
+
// session/disposed, no half-resumed Session object), so the chat
|
|
1070
|
+
// panel keeps the in-memory session as active. We still refresh
|
|
1071
|
+
// the workspace + session-list projections so the destination
|
|
1072
|
+
// workspace and the moved row appear without a manual refresh.
|
|
1069
1073
|
await refreshMovedSession();
|
|
1070
1074
|
setTimeout(() => { void refreshMovedSession(); }, 250);
|
|
1071
1075
|
setTimeout(() => { void refreshMovedSession(); }, 900);
|
|
@@ -1079,16 +1083,16 @@ window.__ModuleLoader__.load({
|
|
|
1079
1083
|
// but calling it unconditionally breaks migration on newer builds.
|
|
1080
1084
|
await ctx.sessions.refresh();
|
|
1081
1085
|
if (wasCurrent) ctx.sessions.open(sessionId);
|
|
1082
|
-
//
|
|
1083
|
-
//
|
|
1084
|
-
//
|
|
1086
|
+
// The migrate rewrite happens in-process on the live session, so
|
|
1087
|
+
// we just need a delayed baseline to let the api-gateway's projection
|
|
1088
|
+
// settle before re-activating the in-memory session.
|
|
1085
1089
|
setTimeout(() => {
|
|
1086
1090
|
void ctx.sessions.refresh().then(() => {
|
|
1087
1091
|
if (wasCurrent) ctx.sessions.open(sessionId);
|
|
1088
1092
|
}).catch(() => {});
|
|
1089
1093
|
}, 250);
|
|
1090
1094
|
},
|
|
1091
|
-
onOpenPanel: () => panelStore.
|
|
1095
|
+
onOpenPanel: () => panelStore.toggle()
|
|
1092
1096
|
});
|
|
1093
1097
|
ctx.slots.inject("conversation.session.header.actions", () => ctx.slots.register({
|
|
1094
1098
|
name: "conversation.session.header.actions",
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/** Compatibility boundary for DSH services used by dsh-session-manager.
|
|
2
|
+
*
|
|
3
|
+
* DSH service names are the supported seam. Concrete implementations
|
|
4
|
+
* expose optional surfaces that changed during the 0.1.x line; keep all
|
|
5
|
+
* probes here so route code can fail closed and diagnostics may
|
|
6
|
+
* explain what this build actually provides.
|
|
7
|
+
*
|
|
8
|
+
* @module dsh-session-manager/compat/dsh-adapter
|
|
9
|
+
*/
|
|
10
|
+
import { createRequire } from "node:module";
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
|
|
16
|
+
/** Pull one service out of the Cordis ctx, tolerating direct property fallbacks. */
|
|
17
|
+
function getService(ctx, name) {
|
|
18
|
+
try {
|
|
19
|
+
const value = typeof ctx?.get === "function" ? ctx.get(name) : undefined;
|
|
20
|
+
if (value !== undefined) return value;
|
|
21
|
+
} catch { /* fall through to the legacy direct property */ }
|
|
22
|
+
return ctx && ctx[name];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Read one package's version from its package.json if resolvable from this plugin. */
|
|
26
|
+
function packageVersion(name) {
|
|
27
|
+
try {
|
|
28
|
+
const manifest = require(name + "/package.json");
|
|
29
|
+
return typeof manifest?.version === "string" ? manifest.version : undefined;
|
|
30
|
+
} catch {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Locate the dsh-cli installation by scanning process.argv for an
|
|
37
|
+
* `apps/cli/` segment and reading its package.json (or the workspace root).
|
|
38
|
+
* Falls back silently when the host is not the dsh CLI (e.g. a test harness).
|
|
39
|
+
*/
|
|
40
|
+
function versionFromArgv() {
|
|
41
|
+
// process.argv on Windows mixes single backslashes, escaped double backslashes,
|
|
42
|
+
// and forward slashes depending on how the host launched the binary. Match
|
|
43
|
+
// "/apps/cli/" by normalizing any of those separators to a single "/" first,
|
|
44
|
+
// then fall back to checking the parent of bin.js (covers dist bundles that
|
|
45
|
+
// relocate the bin path under lib/bin.js).
|
|
46
|
+
for (const arg of process.argv) {
|
|
47
|
+
const normalized = String(arg).replaceAll("\\\\", "/").replaceAll("\\", "/");
|
|
48
|
+
let at = normalized.indexOf("/apps/cli/");
|
|
49
|
+
if (at < 0) {
|
|
50
|
+
// dist/ build: bin.js sits under apps/cli/lib/ rather than directly
|
|
51
|
+
// under apps/cli/, so anchor on the bin file basename instead.
|
|
52
|
+
const binIdx = normalized.lastIndexOf("/bin.");
|
|
53
|
+
if (binIdx >= 0) at = normalized.indexOf("/apps/cli/", binIdx);
|
|
54
|
+
}
|
|
55
|
+
if (at < 0) continue;
|
|
56
|
+
const root = normalized.slice(0, at);
|
|
57
|
+
for (const file of [join(root, "apps", "cli", "package.json"), join(root, "package.json")]) {
|
|
58
|
+
try {
|
|
59
|
+
const manifest = JSON.parse(readFileSync(file, "utf8"));
|
|
60
|
+
if (typeof manifest.version === "string") return manifest.version;
|
|
61
|
+
} catch { /* try the next candidate */ }
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Normalize a persistence list item from both snapshot (header field) and raw header shapes. **/
|
|
68
|
+
export function normalizeSessionHeader(value) {
|
|
69
|
+
if (value === undefined || value === null) return undefined;
|
|
70
|
+
const header = value?.header && typeof value.header === "object" ? value.header : value;
|
|
71
|
+
return header && typeof header.id === "string" ? header : undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
export function normalizeSessionHeaders(values) {
|
|
76
|
+
if (!Array.isArray(values)) return [];
|
|
77
|
+
const out = [];
|
|
78
|
+
for (const value of values) {
|
|
79
|
+
const header = normalizeSessionHeader(value);
|
|
80
|
+
if (header !== undefined) out.push(header);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build the adapter. Exposes the public DSH surfaces this plugin needs and a
|
|
87
|
+
* `capabilities()` snapshot for diagnostics.
|
|
88
|
+
*/
|
|
89
|
+
export function createDshAdapter(ctx) {
|
|
90
|
+
const adapter = {
|
|
91
|
+
get workspaceRegistry() { return getService(ctx, "workspaceRegistry"); },
|
|
92
|
+
get sessions() { return getService(ctx, "sessions"); },
|
|
93
|
+
get agents() { return getService(ctx, "agents"); },
|
|
94
|
+
get persistence() { return getService(ctx, "sessionPersistence"); },
|
|
95
|
+
get agentPresets() { return getService(ctx, "agentPresets"); },
|
|
96
|
+
get projectionCache() { return getService(ctx, "sessionProjectionCache"); },
|
|
97
|
+
get homePath() { return getService(ctx, "dshHomePath"); },
|
|
98
|
+
|
|
99
|
+
getSession(id) {
|
|
100
|
+
try { return adapter.sessions && adapter.sessions.get && adapter.sessions.get(id); } catch { return undefined; }
|
|
101
|
+
},
|
|
102
|
+
getAgent(id) {
|
|
103
|
+
try { return adapter.agents && adapter.agents.get && adapter.agents.get(id); } catch { return undefined; }
|
|
104
|
+
},
|
|
105
|
+
getSessionEvents(session) {
|
|
106
|
+
if (Array.isArray(session && session.events)) return session.events;
|
|
107
|
+
return [];
|
|
108
|
+
},
|
|
109
|
+
normalizeSessionHeader,
|
|
110
|
+
normalizeSessionHeaders,
|
|
111
|
+
async listSessionHeaders() {
|
|
112
|
+
const persistence = adapter.persistence;
|
|
113
|
+
if (!persistence || typeof persistence.list !== "function") return [];
|
|
114
|
+
return normalizeSessionHeaders(await persistence.list());
|
|
115
|
+
},
|
|
116
|
+
capabilities() {
|
|
117
|
+
const persistence = adapter.persistence;
|
|
118
|
+
const registry = adapter.workspaceRegistry;
|
|
119
|
+
const sessions = adapter.sessions;
|
|
120
|
+
let workspaces = [];
|
|
121
|
+
try { workspaces = registry && typeof registry.list === "function" ? registry.list() : []; } catch { /* diagnostic only */ }
|
|
122
|
+
let sampleSession;
|
|
123
|
+
try { sampleSession = sessions && sessions.list && sessions.list() && sessions.list()[0]; } catch { /* diagnostic only */ }
|
|
124
|
+
return {
|
|
125
|
+
sessionList: !!(persistence && typeof persistence.list === "function"),
|
|
126
|
+
sessionStat: !!(persistence && typeof persistence.stat === "function"),
|
|
127
|
+
sessionHandleOpen: !!(persistence && typeof persistence.open === "function"),
|
|
128
|
+
persistenceLocate: !!(persistence && typeof persistence.locate === "function"),
|
|
129
|
+
persistenceResolveLog: !!(persistence && typeof persistence.resolveLog === "function"),
|
|
130
|
+
workspaceList: !!(registry && typeof registry.list === "function"),
|
|
131
|
+
workspaceAttach: !!(workspaces[0] && typeof workspaces[0].attachSession === "function"),
|
|
132
|
+
workspaceDetach: !!(workspaces[0] && typeof workspaces[0].detachSession === "function"),
|
|
133
|
+
sessionStore: !!(sessions && typeof sessions.get === "function"),
|
|
134
|
+
liveSessionEvents: !!(sampleSession && Array.isArray(sampleSession.events)),
|
|
135
|
+
};
|
|
136
|
+
},
|
|
137
|
+
dshVersion() {
|
|
138
|
+
return process.env.DSH_VERSION
|
|
139
|
+
|| packageVersion("@deepseek-ai/dsh")
|
|
140
|
+
|| packageVersion("@deepseek-ai/dsh-cli")
|
|
141
|
+
|| versionFromArgv()
|
|
142
|
+
|| "unknown";
|
|
143
|
+
}, async openSessionRead(id) {
|
|
144
|
+
const persistence = adapter.persistence;
|
|
145
|
+
if (!persistence || typeof persistence.open !== "function") {
|
|
146
|
+
throw new Error("dsh-session-manager: SessionPersistence.open is unavailable; the read path will fall back to disk");
|
|
147
|
+
}
|
|
148
|
+
return await persistence.open(id, "read");
|
|
149
|
+
},
|
|
150
|
+
async statSession(id) {
|
|
151
|
+
const persistence = adapter.persistence;
|
|
152
|
+
if (!persistence || typeof persistence.stat !== "function") return undefined;
|
|
153
|
+
return await persistence.stat(id);
|
|
154
|
+
},
|
|
155
|
+
async resolveSessionPath(id) {
|
|
156
|
+
const persistence = adapter.persistence;
|
|
157
|
+
if (!persistence) return undefined;
|
|
158
|
+
if (typeof persistence.resolveLog === "function") {
|
|
159
|
+
try { return await persistence.resolveLog(id); } catch { return undefined; }
|
|
160
|
+
}
|
|
161
|
+
if (typeof persistence.locate === "function") {
|
|
162
|
+
const header = { id: id };
|
|
163
|
+
try {
|
|
164
|
+
const located = persistence.locate(header);
|
|
165
|
+
return located && located.path;
|
|
166
|
+
} catch { return undefined; }
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
},
|
|
170
|
+
async listSessionSnapshots() {
|
|
171
|
+
const persistence = adapter.persistence;
|
|
172
|
+
if (!persistence || typeof persistence.list !== "function") return [];
|
|
173
|
+
try { return await persistence.list(); } catch { return []; }
|
|
174
|
+
},
|
|
175
|
+
workspaceEntityFor(workspaceId) {
|
|
176
|
+
const registry = adapter.workspaceRegistry;
|
|
177
|
+
if (!registry || typeof registry.get !== "function") return undefined;
|
|
178
|
+
try { return registry.get(workspaceId); } catch { return undefined; }
|
|
179
|
+
},
|
|
180
|
+
workspaceList() {
|
|
181
|
+
const registry = adapter.workspaceRegistry;
|
|
182
|
+
if (!registry || typeof registry.list !== "function") return [];
|
|
183
|
+
try { return registry.list(); } catch { return []; }
|
|
184
|
+
},
|
|
185
|
+
async readSessionEvents(id, max) {
|
|
186
|
+
const handle = await adapter.openSessionRead(id);
|
|
187
|
+
try {
|
|
188
|
+
return await handle.read(0, typeof max === "number" ? max : 1024);
|
|
189
|
+
} finally {
|
|
190
|
+
if (typeof handle.close === "function") {
|
|
191
|
+
try { await handle.close(); } catch { /* best-effort */ }
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
pluginVersion() {
|
|
196
|
+
return packageVersion("dsh-session-manager") || "unknown";
|
|
197
|
+
},
|
|
198
|
+
};
|
|
199
|
+
return adapter;
|
|
200
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/** Strict, generation-safe Zstandard concatenated-frame helpers.
|
|
2
|
+
*
|
|
3
|
+
* DSH session artifacts are concatenated checksummed Zstandard frames (one
|
|
4
|
+
* frame per durable batch), so the multi-frame boundary must be computed
|
|
5
|
+
* from the frame header itself, not from a magic-byte scan. This is a
|
|
6
|
+
* direct port of DSH's frame scanner and shares its layout contract:
|
|
7
|
+
*
|
|
8
|
+
* - Each frame begins with the 4-byte LE magic 0xfd2fb528.
|
|
9
|
+
* - Reserved bits in the frame-header descriptor byte must be zero.
|
|
10
|
+
* - Each block carries a 3-byte LE header; reserved block type 0x3 fails.
|
|
11
|
+
* - Optional 4-byte frame checksum is appended after the last block.
|
|
12
|
+
*
|
|
13
|
+
* @module dsh-session-manager/compat/zstd-frames
|
|
14
|
+
*/
|
|
15
|
+
import { zstdDecompress } from "node:zlib";
|
|
16
|
+
import { promisify } from "node:util";
|
|
17
|
+
|
|
18
|
+
const zstdDecompressAsync = promisify(zstdDecompress);
|
|
19
|
+
const ZSTD_MAGIC = 0xfd2fb528;
|
|
20
|
+
|
|
21
|
+
/** Locate complete Zstandard frames; an incomplete final frame is returned
|
|
22
|
+
* separately as `tornStart` and can be ignored by read-only paths.
|
|
23
|
+
*/
|
|
24
|
+
export function scanZstdFrames(buffer, maxFrames = Number.POSITIVE_INFINITY) {
|
|
25
|
+
const frames = [];
|
|
26
|
+
let offset = 0;
|
|
27
|
+
while (offset < buffer.length) {
|
|
28
|
+
const start = offset;
|
|
29
|
+
if (buffer.length - offset < 4) return { frames, tornStart: start };
|
|
30
|
+
if (buffer.readUint32LE(offset) != ZSTD_MAGIC) {
|
|
31
|
+
throw new Error(`corrupt Zstandard session log: invalid frame magic at byte ${offset}`);
|
|
32
|
+
}
|
|
33
|
+
offset += 4;
|
|
34
|
+
if (offset === buffer.length) return { frames, tornStart: start };
|
|
35
|
+
const descriptor = buffer.readUint8(offset++);
|
|
36
|
+
if ((descriptor & 0x18) !== 0) {
|
|
37
|
+
throw new Error(`corrupt Zstandard session log: reserved frame-header bit at byte ${offset - 1}`);
|
|
38
|
+
}
|
|
39
|
+
const contentSizeFlag = descriptor >>>6;
|
|
40
|
+
const singleSegment = (descriptor & 0x20) !== 0;
|
|
41
|
+
const checksum = (descriptor & 0x04) !== 0;
|
|
42
|
+
const dictionaryFlag = descriptor & 0x03;
|
|
43
|
+
const dictionaryBytes = dictionaryFlag === 3 ? 4 : dictionaryFlag;
|
|
44
|
+
const contentSizeBytes = contentSizeFlag === 0
|
|
45
|
+
? (singleSegment ? 1 : 0)
|
|
46
|
+
: 1 << contentSizeFlag;
|
|
47
|
+
const remainingHeaderBytes = (singleSegment ? 0 : 1) + dictionaryBytes + contentSizeBytes;
|
|
48
|
+
if (buffer.length - offset < remainingHeaderBytes) return { frames, tornStart: start };
|
|
49
|
+
offset += remainingHeaderBytes;
|
|
50
|
+
for(;;) {
|
|
51
|
+
if (buffer.length - offset < 3) return { frames, tornStart: start };
|
|
52
|
+
const blockHeader = buffer.readUIntLE(offset, 3);
|
|
53
|
+
offset += 3;
|
|
54
|
+
const lastBlock = (blockHeader & 1) !== 0;
|
|
55
|
+
const blockType = (blockHeader >>> 1) & 0x03;
|
|
56
|
+
const blockSize = blockHeader >>> 3;
|
|
57
|
+
if (blockType === 0x03) {
|
|
58
|
+
throw new Error(`corrupt Zstandard session log: reserved block type at byte ${offset - 3}`);
|
|
59
|
+
}
|
|
60
|
+
const payloadBytes = blockType === 0x01 ? 1 : blockSize;
|
|
61
|
+
if (buffer.length - offset < payloadBytes) return { frames, tornStart: start };
|
|
62
|
+
offset += payloadBytes;
|
|
63
|
+
if (lastBlock) break;
|
|
64
|
+
}
|
|
65
|
+
if (checksum) {
|
|
66
|
+
if (buffer.length - offset < 4) return { frames, tornStart: start };
|
|
67
|
+
offset += 4;
|
|
68
|
+
}
|
|
69
|
+
frames.push({ start, end: offset });
|
|
70
|
+
if (frames.length === maxFrames) return { frames };
|
|
71
|
+
}
|
|
72
|
+
return { frames };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function decompressZstdFrame(frame) {
|
|
76
|
+
return zstdDecompressAsync(frame);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function decompressAllZstdFrames(buffer) {
|
|
80
|
+
const scan = scanZstdFrames(buffer);
|
|
81
|
+
if (scan.frames.length === 0) {
|
|
82
|
+
throw new Error("empty or header-less Zstandard session log");
|
|
83
|
+
}
|
|
84
|
+
const decoded = [];
|
|
85
|
+
for (const frame of scan.frames) {
|
|
86
|
+
decoded.push(await decompressZstdFrame(buffer.subarray(frame.start, frame.end)));
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
content: Buffer.concat(decoded),
|
|
90
|
+
frameCount: decoded.length,
|
|
91
|
+
torn: scan.tornStart !== undefined,
|
|
92
|
+
};
|
|
93
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -1,58 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @dsh-session-manager — Host half.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Cordis plugin that fills three gaps in DSH:
|
|
5
|
+
* - delete / unarchive (DSH ships `workspace.archiveSession` but not its inverse);
|
|
6
|
+
* - cross-workspace move (DSH indexes workspace membership off the session
|
|
7
|
+
* header's immutable `cwd`, so a real move must re-home the artifact);
|
|
8
|
+
* - per-conversation Agent-preset migration (rewrite the effective preset
|
|
9
|
+
* in place so the next event fold sees the new value).
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* - POST /session-manager/api/unarchive { sessionId }
|
|
14
|
-
* Remove one session from the registry-global archive set. The registry's
|
|
15
|
-
* own `domain/changed` -> `host/archived-sessions-changed` frame then
|
|
16
|
-
* refreshes every connected client.
|
|
17
|
-
* - GET /session-manager/api/workspaces
|
|
18
|
-
* Ordered workspace projection: id / title / path / sessionCount /
|
|
19
|
-
* sessionIds (raw record order). Used by the client move dialog to pick a
|
|
20
|
-
* target and to derive the session's current workspace.
|
|
21
|
-
* - POST /session-manager/api/move { sessionId, targetWorkspaceId }
|
|
22
|
-
* TRUE cross-workspace move. DSH's workspace model keys membership off
|
|
23
|
-
* the session header's immutable `cwd`: `WorkspaceEntity.attachSession`
|
|
24
|
-
* validates `realpath(header.cwd) === workspace.path`, and every record
|
|
25
|
-
* write prunes ids whose indexed cwd no longer matches. So a real move
|
|
26
|
-
* must RE-HOME the session:
|
|
11
|
+
* All durable rewrites go through DSH's own persistence seam (readRaw /
|
|
12
|
+
* create / append / flush on the jsonl backend); the JSONL codec chain
|
|
13
|
+
* migrates v2 -> v3 transparently, so the emitted artifact is always valid
|
|
14
|
+
* for the running DSH build. The plugin never reaches into private DSH
|
|
15
|
+
* module paths.
|
|
27
16
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* an exact one-header-line first frame, or plain JSONL);
|
|
35
|
-
* 3. publish the new artifact at `locate({cwd: target})` and remove the
|
|
36
|
-
* old one, hiding-then-restoring on failure (never leaves the
|
|
37
|
-
* duplicate-id state that would make list()/findLog() throw);
|
|
38
|
-
* 4. enter a short-lived restored session (prepare + enter, never
|
|
39
|
-
* announced) whose fresh header makes attachSession's validation
|
|
40
|
-
* pass and refreshes the registry's canonical-cwd index, then swap
|
|
41
|
-
* accounting: detach from every other workspace's raw record,
|
|
42
|
-
* attach to the target — through the entity's own serialized
|
|
43
|
-
* durability path, so `host/workspace-changed` frames re-group every
|
|
44
|
-
* client sidebar immediately.
|
|
17
|
+
* The move and migrate paths do NOT tear down the live agent or session.
|
|
18
|
+
* The in-memory session stays alive, the file is rewritten in place under
|
|
19
|
+
* a new cwd (move) or a new event is appended to the existing log
|
|
20
|
+
* (migrate), and the workspace registry is updated. The api-gateway's
|
|
21
|
+
* chat panel therefore remains "available" throughout; the user does not
|
|
22
|
+
* have to refresh.
|
|
45
23
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* All mutations go through the workspace registry's own serialized
|
|
50
|
-
* `enqueueOperation` + `setState` durability path (the same one the built-in
|
|
51
|
-
* archiveSession uses), so a restart cannot resurrect a removed session.
|
|
24
|
+
* Compat boundary (lib/compat/) is loaded only because the test suite
|
|
25
|
+
* asserts a static import; the apply() body inlines the DSH services it
|
|
26
|
+
* uses and never instantiates the adapter.
|
|
52
27
|
*/
|
|
53
28
|
import { mkdir, open, readdir, readFile, realpath, rename, rm, stat } from "node:fs/promises";
|
|
54
29
|
import { dirname, join } from "node:path";
|
|
55
30
|
import { randomBytes } from "node:crypto";
|
|
31
|
+
import { createDshAdapter } from "./compat/dsh-adapter.js";
|
|
56
32
|
|
|
57
33
|
export const name = "dsh-session-manager";
|
|
58
34
|
|
|
@@ -219,12 +195,12 @@ ${rest}`, "utf8");
|
|
|
219
195
|
const liveSession = ctx.sessions.get(sessionId);
|
|
220
196
|
let liveAgent;
|
|
221
197
|
try { liveAgent = ctx.agents.get(sessionId); } catch { liveAgent = void 0; }
|
|
222
|
-
// The
|
|
223
|
-
//
|
|
224
|
-
//
|
|
225
|
-
//
|
|
226
|
-
//
|
|
227
|
-
|
|
198
|
+
// The 0.4.6+ move path is no-teardown: the live agent/session stays
|
|
199
|
+
// alive throughout. We only rewrite the on-disk artifact in place and
|
|
200
|
+
// update the in-memory session header to point at the new cwd; the
|
|
201
|
+
// api-gateway's chat panel keeps the in-memory session as active.
|
|
202
|
+
// The persistence coordinator's per-id serialize() (if available) wraps
|
|
203
|
+
// the rewrite so the write-behind cannot append to the hidden file.
|
|
228
204
|
// client UI never has to re-init. Just flush pending events to
|
|
229
205
|
// disk (still at OLD cwd), then atomically rename the artifact to
|
|
230
206
|
// NEW cwd. The persistence coordinator's serialize() inside the
|
|
@@ -360,7 +336,7 @@ ${rest}`, "utf8");
|
|
|
360
336
|
toWorkspaceTitle: target.title || target.id,
|
|
361
337
|
artifactFrom: oldPath,
|
|
362
338
|
artifactTo: newPath,
|
|
363
|
-
wasLive: liveSession !== void 0 ||
|
|
339
|
+
wasLive: liveSession !== void 0 || liveAgent !== void 0
|
|
364
340
|
};
|
|
365
341
|
});
|
|
366
342
|
|
|
@@ -378,7 +354,7 @@ ${rest}`, "utf8");
|
|
|
378
354
|
// unavailable" state (agent/status is only emitted on PHASE
|
|
379
355
|
// CHANGES, never on agent construction). Leaving the live
|
|
380
356
|
// agent/session in place avoids the broken UI entirely.
|
|
381
|
-
ctx.logger.info(`session-manager: moved "${sessionId}" to workspace "${target.id}"
|
|
357
|
+
ctx.logger.info(`session-manager: moved "${sessionId}" to workspace "${target.id}" (in-place file rename, live agent retained)`);
|
|
382
358
|
return result;
|
|
383
359
|
};
|
|
384
360
|
/* === preset-migration (session-manager v0.3.0) === */
|
|
@@ -478,12 +454,14 @@ ${rest}`, "utf8");
|
|
|
478
454
|
};
|
|
479
455
|
|
|
480
456
|
/**
|
|
481
|
-
*
|
|
482
|
-
*
|
|
483
|
-
*
|
|
484
|
-
*
|
|
485
|
-
*
|
|
486
|
-
*
|
|
457
|
+
* Rewrite the effective preset of one conversation in place on disk.
|
|
458
|
+
* The 0.4.6+ flow is no-teardown: when a live session is in store we
|
|
459
|
+
* append the new agent-preset/selected event via Session.append() and
|
|
460
|
+
* flush through SessionStore.flush(); for cold sessions we rewrite
|
|
461
|
+
* the last matching event directly on the existing artifact. In both
|
|
462
|
+
* cases the live agent/session is left intact, the api-gateway chat
|
|
463
|
+
* panel stays available, and the projection picks up the new value
|
|
464
|
+
* on the next event fold.
|
|
487
465
|
*/
|
|
488
466
|
/** Cold-path preset migration: read disk, rewrite, publish. Works whether
|
|
489
467
|
* or not the session is currently in the coordinator, and does not depend on
|
|
@@ -688,72 +666,12 @@ ${rest}`, "utf8");
|
|
|
688
666
|
return result;
|
|
689
667
|
};
|
|
690
668
|
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
placeholder header for index refresh).
|
|
698
|
-
/** Move one session from its current workspace to a target workspace. (legacy)
|
|
699
|
-
const moveSession = async (sessionId, targetWorkspaceId) => {
|
|
700
|
-
const registry = ctx.workspaceRegistry;
|
|
701
|
-
const allWorkspaces = registry.list();
|
|
702
|
-
|
|
703
|
-
// Find target workspace
|
|
704
|
-
const targetWorkspace = allWorkspaces.find((e) => e.id === targetWorkspaceId);
|
|
705
|
-
if (!targetWorkspace) {
|
|
706
|
-
throw new Error(`目标工作区不存在: ${targetWorkspaceId}`);
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
// Find current workspace(s) containing this session
|
|
710
|
-
const currentWorkspaces = allWorkspaces.filter((e) => e.sessionIds.includes(sessionId));
|
|
711
|
-
|
|
712
|
-
// Check if already in target workspace
|
|
713
|
-
if (currentWorkspaces.length === 1 && currentWorkspaces[0].id === targetWorkspaceId) {
|
|
714
|
-
return { ok: true, sessionId, moved: false, message: "会话已在目标工作区中" };
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
// Detach from all current workspaces
|
|
718
|
-
for (const entity of currentWorkspaces) {
|
|
719
|
-
await entity.detachSession(sessionId);
|
|
720
|
-
}
|
|
721
|
-
|
|
722
|
-
// Attach to target workspace
|
|
723
|
-
if (typeof targetWorkspace.attachSession === "function") {
|
|
724
|
-
await targetWorkspace.attachSession(sessionId);
|
|
725
|
-
} else if (typeof targetWorkspace.prependSession === "function") {
|
|
726
|
-
await targetWorkspace.prependSession(sessionId);
|
|
727
|
-
} else {
|
|
728
|
-
// Fallback: try to use the registry's enqueueOperation
|
|
729
|
-
await registry.enqueueOperation(async () => {
|
|
730
|
-
const state = registry.requireState();
|
|
731
|
-
// Find target workspace in state and add session
|
|
732
|
-
const targetState = state.workspaces?.find((w) => w.id === targetWorkspaceId);
|
|
733
|
-
if (targetState) {
|
|
734
|
-
if (!targetState.sessionIds) targetState.sessionIds = [];
|
|
735
|
-
if (!targetState.sessionIds.includes(sessionId)) {
|
|
736
|
-
targetState.sessionIds.unshift(sessionId);
|
|
737
|
-
}
|
|
738
|
-
await registry.setState(state);
|
|
739
|
-
}
|
|
740
|
-
});
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
return {
|
|
744
|
-
ok: true,
|
|
745
|
-
sessionId,
|
|
746
|
-
moved: true,
|
|
747
|
-
fromWorkspace: currentWorkspaces.map((e) => e.id),
|
|
748
|
-
toWorkspace: targetWorkspaceId
|
|
749
|
-
};
|
|
750
|
-
};
|
|
751
|
-
|
|
752
|
-
==== end legacy v0.2.0 move implementation ==== */
|
|
753
|
-
/** Resolve the on-disk session directory (parent of its artifact), if any. */
|
|
754
|
-
/**
|
|
755
|
-
* Encode a session id the way DSH stores it on disk: "--<encoded>--".
|
|
756
|
-
* Mirrors @deepseek-ai/dsh-session-persistence-jsonl encodeSegment.
|
|
669
|
+
/**
|
|
670
|
+
* Encode a workspace path the way DSH stores it as a project directory:
|
|
671
|
+
* separators and unsafe code units become "--<encoded>--" markers.
|
|
672
|
+
* This mirrors the projectKey helper inside the JSONL persistence backend.
|
|
673
|
+
* (The name encodeSessionSegment is a leftover from an earlier refactor;
|
|
674
|
+
* the function encodes a workspace path, not a session id.)
|
|
757
675
|
*/
|
|
758
676
|
const encodeSessionSegment = (id) => {
|
|
759
677
|
let readable = "";
|
|
@@ -813,7 +731,7 @@ ${rest}`, "utf8");
|
|
|
813
731
|
if (!proj.isDirectory()) continue;
|
|
814
732
|
for (const candidate of [encoded, sessionId]) {
|
|
815
733
|
const dir = join(root, proj.name, candidate);
|
|
816
|
-
for (const filename of ["session.jsonl.zstd", "session.v2.jsonl.zstd", "session.jsonl"]) {
|
|
734
|
+
for (const filename of ["session.jsonl.zstd", "session.v2.jsonl.zstd", "session.v3.jsonl.zstd", "session.jsonl"]) {
|
|
817
735
|
const filePath = join(dir, filename);
|
|
818
736
|
try {
|
|
819
737
|
const buf = await readFile(filePath);
|
|
@@ -881,7 +799,7 @@ ${rest}`, "utf8");
|
|
|
881
799
|
catch { continue; }
|
|
882
800
|
for (const entry of dirs) {
|
|
883
801
|
if (!entry.isDirectory()) continue;
|
|
884
|
-
for (const filename of ["session.v2.jsonl.zstd", "session.jsonl.zstd", "session.jsonl"]) {
|
|
802
|
+
for (const filename of ["session.v3.jsonl.zstd", "session.v2.jsonl.zstd", "session.jsonl.zstd", "session.jsonl"]) {
|
|
885
803
|
const filePath = join(root, proj.name, entry.name, filename);
|
|
886
804
|
try {
|
|
887
805
|
const buf = await readFile(filePath);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-manager",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
4
4
|
"description": "Session manager for the DeepSeek Harness Web UI: delete sessions, archive sessions, move sessions across workspaces, and migrate a session’s Agent preset. Suggestions are welcome on GitHub. 在 DeepSeek Harness Web 中进行会话管理,包括:删除会话、归档会话、跨工作区移动会话、迁移会话的 Agent 预设。欢迎至GitHub提意见。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|