Introduction¶
When using DSH, the project assignment of a conversation sometimes needs to be adjusted afterwards: this work started in Project A, but was later found to belong to Project B; or wanting to move a conversation from “Scaffold” to a formal project. The official API only supports sorting within the same workspace (insertSessionBefore requires the session to be committed), and the session’s JSONL header is read-only and cannot be changed for cwd.
dsh-session-mover solves this problem. It is a client plugin maintained by railgun20001; dragging sessions grouped by project in the sidebar onto another project title changes their ownership. It is explicitly not a file migration—the session’s JSONL logs and attachments remain in the original project directory; the plugin only modifies the workspace assignment ledger and projects the read side onto the new project.
Below, I introduce its capabilities, installation methods, and usage boundaries.
Positioning and Core Mechanism¶
The plugin records “which project a session belongs to” in two places: the sessionIds in each workspace record (persistent ledger), and the registry in-memory index sessionPaths. It treats the ledger as authoritative:
- When moving, first point the in-memory index to the target directory, then rewrite the records for both workspaces;
- On startup, refill the index with the assignment from the ledger, ensuring the move persists across restarts;
- Fix the index again before writing to the official ledger (write-before-fix), to prevent it from being pruned by the official logic.
Installation and Activation¶
The npm package name, repository name, and internal plugin id are all dsh-session-mover.
Install from npm:
dsh plugin --profile web add dsh-session-mover
Install from source:
git clone https://github.com/railgun20001/dsh-session-mover.git
dsh plugin --profile web add "link:C:/work/dsh-session-mover"
You can also select a local directory by clicking “Add Plugin” on the “Plugins” page in DSH. The host half (index.js) is hot-loaded with the profile, while the client half (client.js) requires a page refresh (Ctrl+R).
Typical Usage¶
- Sessions are displayed grouped by project in the sidebar (default view);
- Hold any conversation and drag it onto the title of another project;
- The target project lights up with a border; releasing the mouse completes the move.
There are a few differences in drop targets: dragging onto a specific session row inserts it before that session; dragging onto the project title places it at the front. Dragging to the top or bottom edges of the sidebar list causes automatic scrolling; the closer to the edge, the faster it scrolls, so you don’t need to manually flip pages in long lists across groups. Dragging back to the original project or within the same group remains the official manual sort; the plugin does not intervene.
After a successful move:
- The conversation appears immediately under the target project, and it is no longer listed in the original project;
- If the conversation is already loaded, the working directory is switched to the target project directory, and subsequent tool calls execute in the new directory;
- If not yet loaded, the ownership is effective but the working directory remains unchanged; the console logs will explain this;
- New conversations forked from moved sessions also belong to the target project, aligning with the parent session’s ledger, index, and working directory.
The plugin does not show a toast; the move result is written to the browser console (console.info / console.warn).
Managed Sessions and Self-Check¶
Sessions that have been moved and whose header.cwd is still stuck in the original directory are called managed sessions; the plugin records them one by one in the move journal:
$DSH_HOME/dsh-session-mover/journal-v1.json
When DSH_HOME is not set, it is ~/.dsh. The log only stores “session id → ledger directory / original directory / time / source” and does not contain conversation content. To view the managed list:
__DSH_SESSION_MOVER__.managed()
// or: GET /dsh-session-mover/api/managed
The host prints a self-check summary line on every startup:
[dsh-session-mover] Self-check: 3 managed sessions (0 missing target directories, 1 original directories no longer exist)
Missing target directory means the file tree and tool calls will report ENOENT; Original directory does not exist means the session files may have disappeared with the old folder. Both cases will warn one by one.
Must Drag Back Before Uninstalling or Disabling¶
This is the most important usage convention for this plugin: the ownership ledger has been modified, but the member filtering based on “which project a session belongs to” still relies on header.cwd. When the plugin is present, it fills in this gap. When the plugin is absent (uninstalled, disabled, load failure, incompatible upgrade), moved sessions will disappear from all projects and fall into “Unassigned”. This is not data loss: re-enabling the plugin or dragging the session back to any project will restore it. Profiles other than web/webServer (e.g., pure CLI) do not load this plugin, so the original project assignment is always visible.
To return to the original project, simply drag the session back; the plugin does not provide an additional “Restore” command.
Two Barriers on the Write Path¶
The plugin does not satisfy the official invariant that “header.cwd must equal the workspace directory,” so the official write path has two places where it will trip over moved sessions:
- Write-Before-Fix: The official
WorkspaceEntity.mutate()prunes based on the in-memory indexsessionPaths. Sessions not in that directory are deleted from the ledger and persisted. The barrier fixes the index according to the move journal before writing, avoiding silent exclusion of managed sessions due to unrelated operations like renaming, sorting, deleting, or creating sessions. - Attach Takeover: The official
attachSession()requiresheader.cwdto equal the target project directory, otherwise it throws an error. The barrier takes over according to the ledger after a failure. It bypasses the check only if it can prove the session truly belongs to this project (ledger points to it, or it is a fork of a session in this project); otherwise, it throws the error as usual.
Two Seams on the Read Side¶
There is a third reader for ownership: any interface that derives the directory based on the session’s cwd. Instead of rewriting the session file, the plugin patches two seams:
- Host side: Uses typert’s effect-scoped override point to take over the
workspaceFileScopelookup parameters, causing the file system interface to parse the workspace root based on the ledger rather than the header. - Client side: The host opens a read-only
/api/ownership, and the client half overridescwdwith the ledger directory at the write point of the session list snapshot, immediately replaying the snapshot after the move is complete, without waiting for a page refresh.
The only place where session logs are written is “working directory switching”: when the session is loaded, the plugin calls the official workingDirectory.set() and appends a working-directory/change event; when not loaded, it skips and writes nothing.
Known Boundaries¶
- Not a file migration: Session JSONL, attachments, and crash recovery directories remain in the original project directory. Deleting or renaming the old project folder, backing up by directory, or migrating to a new machine will miss sessions that have been “moved.”
- Read side only covers two places:
workspaceFileScopeand the client session list snapshot. ACP/IDE session lists, CLI, third-party plugins, exports, and backups still look atheader.cwd. - Old absolute paths in historical messages: Under the new project’s
workspaceRoot, they become out of bounds (workspace-file/outside-workspace). - When target directory disappears: The official
workingDirectory.ensure()will pull the working directory back to the old project; the self-check only warns and does not auto-fix. - Mutually exclusive with “True Migration” plugins: For example,
dsh-workspace-movermoves files and rewrites the header; having both installed will inevitably conflict.
Applicable Scenarios¶
Suitable for DSH users who want to manage sessions grouped by project and are willing to accept the “ownership separated from file location” model. It runs with the current dsh process permissions. Before installing, you should check the plugin source code and license (MIT). The plugin performs runtime probing of the internal structure of WorkspaceEntity; if the structure does not match, the move will fail with an explanation and will not write bad data. Both the read side and the barriers handle “fail and degrade.”
The source code includes a test matrix (test/core-test.mjs, scroll-test.mjs, view-test.mjs, client-test.mjs, fork-test.mjs, guard-test.mjs, managed-test.mjs, workdir-test.mjs). See docs/acceptance.md for the actual device acceptance matrix.
Summary¶
For the scenario of “sessions started in the wrong project group,” dsh-session-mover solves the ownership problem with minimal changes: dragging changes the ledger, doesn’t move files, and can be dragged back to restore.
The trade-off is that session files remain in the original directory, and moved sessions must be dragged back before uninstalling or disabling the plugin.
- Directory page: https://www.skillhub.cn/plugins/railgun20001/dsh-session-mover
- GitHub: https://github.com/railgun20001/dsh-session-mover