dsh-workspace-mover 2.4.2 → 2.4.3

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.
@@ -1,224 +1,224 @@
1
- # Architecture
2
-
3
- Technical reference for contributors and advanced users. Product-facing install/usage docs live in [README.md](../README.md) / [README_EN.md](../README_EN.md). This file is the **single source of truth** for implementation details; do not duplicate it into READMEs.
4
-
5
- ## Plugin mount (DSH 0.1.5)
6
-
7
- - Package is pure JavaScript source-as-product (no TypeScript, no build step).
8
- - Host half mounts through a standard `insert` row in [`cordis.patch.yml`](../cordis.patch.yml).
9
- - Client half is injected via `package.json` → `dsh.client` (`@deepseek-ai/dsh-client-connection`, `@deepseek-ai/dsh-client-ui-slots`, platform `web`).
10
- - Official bundles are never rewritten; only event listeners and official slots are used.
11
- - From GitHub, pnpm runs no install scripts, so `allowBuilds` is not required.
12
-
13
- ## Host / Client split
14
-
15
- | Half | Entry | Role |
16
- | --- | --- | --- |
17
- | Host | `lib/index.js` | RPC, disk archives, workspace registry, backups, trash, history, locks |
18
- | Client | `client/client.js` | Sidebar drag intercept, multi-select, confirmation UI, rescue panel (settings slot) |
19
-
20
- Client locates rows by ARIA semantics only:
21
-
22
- - session rows: `[aria-selected]`
23
- - workspace title rows: `[aria-expanded]`
24
-
25
- It never depends on CSS-module hash class names. UI uses official design tokens (`--dsw-alias-*`) for theme adaptation.
26
-
27
- Only cross-group drops are intercepted; official same-group reorder stays untouched. After a successful move the client actively refreshes and reconciles the official workspace projection; a page reload remains the user-facing fallback.
28
-
29
- ## RPC channel
30
-
31
- Single logical channel `/workspace-mover` via `ctx.connection.rpc.handle('/workspace-mover', …)`. No independent REST routes.
32
-
33
- | Endpoint | Purpose |
34
- | --- | --- |
35
- | `mover.status` | Capability report (detected services / degraded items) plus `storageLayout`, `degradedFeatures`, and the `unarchiveApi` / `setTitleApi` flags |
36
- | `mover.workspaces` | List workspaces |
37
- | `mover.move` | Single-session true move |
38
- | `mover.preview` | Read-only migration preview: source/target, collision, running state, adapter and estimated new snapshot bytes; `backup.reused` identifies an existing matching snapshot |
39
- | `mover.moveMany` | Bulk move (≤ `batchLimit`, default 50) |
40
- | `mover.scan` | Classified rescue scan (parses the newest `scanLimit`, default 400; reports `scanned` vs `scannedParsed`) |
41
- | `mover.repair` | Attach / relink / home one item |
42
- | `mover.repairAll` | Batch repair (per-item isolation) |
43
- | `mover.history` | Recent moves (≤ `historyLimit`, default 100) |
44
- | `mover.undo` | Undo one history entry |
45
- | `mover.ws.audit` | Workspace path health |
46
- | `mover.ws.create` | Register a new workspace (`name`, `directory`) over an existing folder; `conflict` when the name or the folder is already in use |
47
- | `mover.ws.rename` | Rename one workspace (`workspaceId`, `name`, optional `dryRun`); display title only, `conflict` on a name another workspace holds |
48
- | `mover.repoint` | Move-home wizard |
49
- | `mover.archived` / `mover.unarchive` | List / restore archived sessions (`mover.archived` also reports `truncated` / `scannedParsed` / `scannedTotal` / `archivedTotal`) |
50
- | `mover.openFolder` | Open group directory in OS file manager |
51
- | `mover.session.delete` | Session → recycle bin; optional `purgeBackups: true` also deletes that session's migration backups |
52
- | `mover.trash.list` / `mover.trash.restore` / `mover.trash.purge` | Recycle bin; `purge` accepts optional `purgeBackups: true` |
53
- | `mover.backups.list` / `mover.backups.restore` / `mover.backups.deleteOne` | Recovery-snapshot management; `list` reports `orphan` per group, physical `count`, plus `orphanGroups` / `orphanBytes` |
54
- | `mover.tasks.list` / `mover.tasks.retry` / `mover.tasks.forget` | Migration task center |
55
- | `mover.migrations.enqueue` / `mover.migrations.list` / `mover.migrations.retry` / `mover.migrations.cancel` | Persistent busy-session queue; `retry` accepts one `taskId` or a `taskIds` batch, with per-item failure isolation |
56
- | `mover.bundle.export` / `mover.bundle.inspect` / `mover.bundle.import` | Portable Bundle export, dry-run inspection, and import through the adapter/backup pipeline |
57
- | `mover.data.cleanup` | Dry-run and clean aged plugin data (default age = `cleanupDays`) |
58
-
59
- ### Tunable policies
60
-
61
- `mover.data.cleanup`'s age and the caps above come from `resolvePolicy()` in `lib/index.js`, read **at the point of use** (never frozen at module load):
62
-
63
- | Policy | Default | Environment override |
64
- | --- | --- | --- |
65
- | physical snapshot retention per session | 20 | `DSH_WORKSPACE_MOVER_BACKUP_KEEP` |
66
- | move history length | 100 | `DSH_WORKSPACE_MOVER_HISTORY_LIMIT` |
67
- | batch cap (move / repair) | 50 | `DSH_WORKSPACE_MOVER_BATCH_LIMIT` |
68
- | repoint cap | 200 | `DSH_WORKSPACE_MOVER_REPOINT_LIMIT` |
69
- | rescue scan cap | 400 | `DSH_WORKSPACE_MOVER_SCAN_LIMIT` |
70
- | cleanup age (days) | 30 | `DSH_WORKSPACE_MOVER_CLEANUP_DAYS` |
71
-
72
- An unparseable or out-of-range value falls back to the default, so a bad configuration can never disable a feature.
73
-
74
- ### Dependency declaration, and why every peer is `*`
75
-
76
- `package.json` declares the eight host packages this plugin integrates with as `peerDependencies`, all `*` and all `optional` in `peerDependenciesMeta`. They are **declarative metadata for storefronts**, not a compatibility gate: the plugin fail-softs at runtime and reports any real degradation through `mover.status` capabilities and `mover_doctor`. The real floor is `engines.dsh` (`>=0.1.5-rc.1`) plus `engines.node`.
77
-
78
- This was not the first attempt. The ranges originally used the ecosystem's "explicit prerelease branch" idiom, e.g. `>=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0`, and the host then reported the plugin as **incompatible with `0.2.0-rc.2`** — its own tested host line. The mechanism, confirmed by running dsh-market's own range evaluator:
79
-
80
- - npm's prerelease rule is evaluated at the **comparator-set level**: one `||` alternative is one set, and a prerelease version satisfies that set only when at least one comparator in it shares the version's exact `[major, minor, patch]` tuple *and* carries a prerelease tag; only then are all comparators checked normally.
81
- - `<0.2.0-0` does share the `0.2.0` tuple and does carry a prerelease, so the set is admitted — but `0.2.0-rc.2 > 0.2.0-0` (the `-0` sentinel sorts below every real prerelease), so the bound then excludes **every** 0.2.0 prerelease.
82
-
83
- Enumerating release lines reproduces the fault on each new line: a `<0.3.0-0` bound breaks the moment a 0.3 prerelease ships. Hence `*`. dsh-market filters peer declarations to `/^@deepseek-ai\/dsh(?:-|$)/`, which is also why `@deepseek-ai/cordis` and `@deepseek-ai/schemastery` were never named in the warning — they are only evaluated when `engines.dsh` mentions them, and it does not.
84
-
85
- `test/policy.test.mjs` asserts that the declared ranges plus `engines.dsh` derive `compatible` — never `incompatible` — for DSH versions from `0.1.5-rc.1` through `1.0.0`.
86
-
87
- **Official Config is deliberately not registered.** The schema would need `@deepseek-ai/schemastery`, which the host loader provides but which is not resolvable from this plugin's directory (verified by `createRequire` from `lib/index.js`); the npm `schemastery` has no `.volatile()` either. Because a bundle-layer static import failure breaks **boot** (a documented ecosystem failure class), `lib/config.js` never imports it — it exposes `buildConfig(z)` / `attachConfig(moduleOrBuilder)` so a host that hands over a schema builder can attach one, and exports `Config === undefined` otherwise. `buildConfig` contains all its own failures and returns `undefined` rather than throwing.
88
-
89
- ### Official-API preference is observable
90
-
91
- v2.2 prefers two documented host APIs and keeps an equivalent fallback for each: `registry.unarchiveSession()` (fallback: `enqueueOperation` + `setState`) and `entity.setTitle()` (formerly folded into `entity.mutate`). Both fallbacks are silent by design — the feature still works — so `mover.status.capabilities` reports `unarchiveApi` / `setTitleApi` and lists `unarchive-official-api` / `set-title-official-api` in `degradedFeatures` when the host lacks them. That is what makes "which path did it take?" answerable on a real machine instead of a guess.
92
-
93
- Errors carry stable codes (`busy`, `conflict`, `not-found`, `rollback-failed`, …). Host logs write failures under `MOVE FAILED`.
94
-
95
- ## Agent tools (DSH ≥ 0.1.5 with `dsh-tools`)
96
-
97
- When the host provides the `tools` service, `apply()` registers six plain (zero-dependency) tool definitions; each tool's execute re-enters the internal RPC dispatch, so agent calls share the panel's locks, error codes, and transaction semantics:
98
-
99
- | Tool | Gate | Purpose |
100
- | --- | --- | --- |
101
- | `mover_list_sessions` | none (read-only) | Compose scan + workspace list (sessions capped at 40, counts, ghosts, recoveryCount) for id resolution |
102
- | `mover_move_session` | user approval | Move one session (`sessionId`, `targetWorkspaceId`, optional `sessionTitle`) |
103
- | `mover_repair_sessions` | user approval | One `mover.repairAll` pass (accounting-only) |
104
- | `mover_doctor` | none (read-only) | One `mover.doctor` self-check pass (host services, data dirs, recovery records, workspace paths) |
105
- | `mover_create_workspace` | none | Register a workspace (`name`, `directory`) through `mover.ws.create` |
106
- | `mover_rename_workspace` | user approval unless full access | Rename a workspace (`workspaceId`, `name`) through `mover.ws.rename` |
107
-
108
- - Mutating tools call `approval.request({ agent, toolName, callId, reason, signal })` before acting; `allowed-once` proceeds, anything else (`denied` / `cancelled` / `unavailable`) or a throwing seam fails closed. Hosts without an approval seam refuse operations requiring approval and direct the user to the rescue panel. `callId` is the tool call's own id. The ACP answerer delegates any request without one, so ACP sessions could never approve before it was passed, and the Web prompt uses it to attach to the tool call it already shows.
109
- - `mover_rename_workspace` skips the prompt when the calling session's effective sandbox mode, read from `ctx.sandboxPolicy.resolve({ session })`, is `danger-full-access` — the same test the official `plugin_manager` tool uses. The bypass is required, not only convenient: the shipped `danger-full-access` preset pairs that mode with approval policy `never`, under which every request resolves `rejected`. A missing policy service or a throwing `resolve` counts as *not* full access, so the tool asks. The tool validates through `dryRun` first, so an unknown id, a blank name, or a name conflict fails before the user is asked; renaming to the current title is a no-op that never prompts. The commit re-validates inside the serialized naming queue, so a conflict created while the prompt was open is still rejected. `mover_doctor` reports whether full-access detection is available under `host-sandbox-policy`.
110
- - `mover_create_workspace` is ungated because it only adds a registry entry for an existing folder: it never creates folders or files, and a directory already used by a workspace is rejected. "Same folder" uses the host's own identity. The official `registry.create()` canonicalizes with `realpath` and returns the *existing* entity for an owned path, so a trailing slash, `..`, or a symlink alias counts as the same folder. The plugin treats "returned an entity that existed before the call" as `conflict` instead of re-implementing path comparison.
111
- - Workspace names must be unique (exact match after trimming) for both create and rename. The host registry allows duplicate titles, but the official rename dialog refuses them; the plugin applies that rule to both tools. Check and write run in one serialized queue so two calls cannot claim the same name.
112
- - `exec.signal` propagates into the RPC layer; cancellation lets the in-flight step finish rolling back to a consistent state.
113
- - Hosts without `dsh-tools` skip registration silently; `mover.status` reports the `agentTools` flag.
114
- - **Only `workspaceRegistry` and `sessionPersistence` are in `inject`.** Cordis treats every `inject` entry as required: a missing one leaves the whole plugin fiber inactive, which removes the rescue panel's RPC channel along with every tool. `tools` and `approval` were listed until 2.3.x, so a host without `dsh-user-approval` lost the plugin entirely, and the old missing-approval branch could never run. Now the tools register inside a nested `ctx.inject(['tools'], …)`, so they appear and disappear with `dsh-tools` and the `agentTools` flag follows. Missing approval now blocks operations that require confirmation. The approval service is read with `ctx.get('approval')` on each call, so installing or removing `dsh-user-approval` takes effect without re-mounting the plugin.
115
-
116
- ## Session archives and storage adapter
117
-
118
- - The fallback filesystem adapter recognizes names `session.jsonl[.zstd]` and `session.vN.jsonl[.zstd]`; it selects the highest generation without rewriting older files to v3.
119
- - A mixed plain/Zstd directory and a file whose compression differs from the host's configured mode are rejected explicitly. Mixed compression elsewhere under the same root is diagnosed per session, not by a root-wide scan.
120
- - v3/v4 filenames are recognized, but a v4 runtime fixture has not yet been verified. Filename/header mismatch is rejected only when the header contains one of the generation fields currently recognized by the adapter.
121
- - True move keeps the session id and all frames; only the header `cwd` (first frame) is rewritten; other frames stay byte-identical. Publish via temp file + atomic rename.
122
-
123
- ### The storage-layout assumption, and its self-check
124
-
125
- The adapter first reports the public `list` / `stat` / `open` surface. When a provider also exposes a physical root, the plugin uses the legacy directory adapter as a visible `filesystem-fallback`; when only official handles are available it stays read/diagnostic capable and marks physical moves as `unsupported` instead of guessing a path.
126
-
127
- `verifyStorageLayout(ctx)` closes that gap: it takes up to five real headers from `persistence.list()`, reverse-derives each session's directory with this plugin's own encoding, and confirms an archive is actually there. Result states are `ok` / `degraded` / `empty` (no sessions to verify) / `unavailable`. It is exposed as `mover.status.capabilities.storageLayout` plus a `data-layout` check in `mover_doctor`, is strictly read-only, and never throws (any error degrades to a warning).
128
-
129
- `mover.status.capabilities.storageAdapter` is `filesystem-fallback`, `official-handle`, or `unavailable`; `storageApi`, `physicalPath`, and `migrationQueueState` expose the exact degradation and queue state. `mover_doctor` reports the same adapter under `storage-adapter` and checks the durable migration records under `data-migrations`.
130
-
131
- Portable Bundles contain `manifest.json`, `sessions/`, `attachments/`, `checksums.json`, and `workspace-metadata.json`. Import defaults to dry-run and reports id conflicts, missing attachments, compression incompatibilities, required/free space, and recoverable files. Execution refuses a blocked plan, backs up each artifact, verifies the rewritten header, and isolates failures per session. Attachment files are preserved and copied into the imported session directory; the bundle report calls out any missing file.
132
-
133
- ## Move pipeline
134
-
135
- Order of operations for a cross-workspace move:
136
-
137
- 1. **Running check** — queue running or resident sessions; a resident backend writer cannot be safely rebound through the current public API. Await backend `flush()` when available, then recheck residency before reading the archive.
138
- 2. **Read authority header** from disk; verify target ≠ source.
139
- 3. **Recovery snapshot** to `$DSH_HOME/workspace-mover/backups/` (up to `backupKeep` distinct physical files per session, default 20) **before any side effect**. The content fingerprint ignores the migration-only header `cwd`, so a move back and forth reuses the existing snapshot. A backup failure aborts with `backup-failed` and nothing has been touched.
140
- 4. **Preflight** — dual-accounting detect, target writability probe, disk-space hint.
141
- 5. **Rewrite header cwd**; if an existing `working-directory/change` would override it, append a new event with the next contiguous sequence number. Preserve original events. Reject unsupported generations or invalid sequences before detach.
142
- 6. **Move directory** (Windows: exponential-backoff retry on EPERM; fallback copy+delete).
143
- 7. **In-memory closeout** — invalidate three registry indexes before official `attachSession`; never change private writer state or live session headers. Clear the cached `workingDirectory` row so it rebuilds from the migrated log.
144
- 8. **Accounting** — source `detachSession` then target `attachSession`.
145
- 9. **Verify** — read archive back; id + cwd must match or the move fails.
146
- 10. **Rollback on any failure** — restore original bytes back to source dir and reattach the source workspace.
147
-
148
- Concurrency: same session / same target workspace try-acquire a lock. A running or resident session returns `busy` to the transaction layer and is persisted in `migrations.json` as `waiting-for-idle`; the queue wakes every few seconds and on the next plugin start. Idle alone does not release a resident writer: wait for unloading or restart. Workspace repoint checks residency before changing the workspace path. Bulk moves de-duplicate first and isolate each result.
149
-
150
- If “rollback also failed”, a durable recovery record is written and shown as a red manual-recovery row. Session files and backups stay in place — never auto-deleted.
151
-
152
- ## Workspace repoint (move-home)
153
-
154
- Official workspace entity writes prune members using in-memory session cwd indexes. The wizard:
155
-
156
- 1. Audits path validity (`mover.ws.audit`).
157
- 2. Pre-seeds all affected sessions’ three indexes to the new path.
158
- 3. Rewrites the entity path through the unified `mutate` channel so members are not pruned.
159
- 4. Batch-migrates member sessions and strays from the old path through the same move pipeline.
160
- 5. Running sessions skip; an interrupted run resumes with only the remainder.
161
-
162
- If `mutate` is unavailable after a host upgrade, the wizard aborts before the first file change.
163
-
164
- ## Plugin-owned data
165
-
166
- All plugin metadata lives under `$DSH_HOME/workspace-mover/` and never overwrites official session archives in place except through the move pipeline.
167
-
168
- | Path | Use |
169
- | --- | --- |
170
- | `backups/` | Byte-level recovery snapshots; new files include a SHA-256 content fingerprint, while legacy timestamp-only names remain readable |
171
- | `history.json` | Last 100 cross-workspace moves |
172
- | trash / task / recovery files | Recycle bin manifest, task center, recovery ledger |
173
-
174
- Official session files and workspace registry are only mutated via host APIs: registry `mutate` / `attachSession` / `detachSession` and durable domain state.
175
-
176
- ## Compatibility boundary
177
-
178
- Verified (each entry dated; the newest verification supersedes the badge claims in the READMEs):
179
-
180
- - DeepSeek Harness `0.2.0-rc.2` (desktop, 2026-10-04) — see `docs/compatibility/dsh-0.2.0-rc.2-2026-10-04.md`
181
- - DeepSeek Harness `0.1.5-rc.1` / `0.1.5-rc.2` (earlier release line)
182
- - dsh-market `1.45.1`
183
- - Node.js `≥ 22`
184
- - `package.json` → `engines.dsh`: `>= 0.1.5-rc.1` (a lower bound only; it is deliberately not an upper bound, and dsh-market reads it for its host-requirement card)
185
-
186
- GitHub-only packages may show “host requirement unknown” in dsh-market when no npm manifest is published; that is metadata, not runtime incompatibility.
187
-
188
- Version-sensitive host behaviors degrade with explicit UI messages (not silent failure):
189
-
190
- - Unarchive needs the registry durable state channel.
191
- - Projection-cache titles parse defensively against v3; missing file falls back to archive header title.
192
- - Host upgrades that rename registry cache fields hit the degrade path (feature still usable; ownership refresh may need a restart).
193
-
194
- ## Coexistence notes (implementation)
195
-
196
- - CSS classes `wsm-*` and DOM attributes `data-wsm-*` are private namespaces.
197
- - Settings panel uses the official `settings.section` slot.
198
- - Sidebar redraw plugins that replace workspace DOM can break ARIA row lookup — features stop triggering; data is not damaged.
199
- - Do not install a second document-level drag interceptor for cross-workspace moves.
200
-
201
- ## Client-half invariants
202
-
203
- - **Every document-level listener and the menu `MutationObserver` is registered through the `listen()` helper and released in one `ctx.effect` teardown**, and `apply` is guarded by a module-level `applied` flag. The desktop client disposes and recomposes the plugin fiber on hot reload; without both, a second copy of every listener stacks up (duplicate confirm dialogs, duplicate RPC, two closures fighting over the same selection highlight).
204
- - **A row is only ever mapped to a session id by a verifiable credential.** The authoritative path walks the React fiber to `memoizedProps.node.id`; the fallback aligns DOM rows to `workspace.sessionIds` by exact/longest title match. A row that cannot be confidently mapped is reported, never substituted — the previous "take the next id in order" behavior could move a different, valid session that the host cannot detect.
205
- - **A workspace header is only mapped to a registry entity when the header text contains that entity's title or path** (or when no entity carries a display identity at all). `registry.list()` includes workspaces whose folder is gone while the sidebar hides them, so positional mapping is unsafe.
206
- - `rowSessionId`'s fiber guard is intentionally permissive; if the official node shape changes, the fallback engages and the user is told, rather than the feature silently doing nothing.
207
- - Dialogs are built by `injectOverlay()` and handed to `openDialog()`, which appends **before** focusing (an element not in the document cannot take focus in a browser), focuses the primary button, and traps Tab within the card.
208
-
209
- ## Tests & release
210
-
211
- - `npm test` runs `test/core.test.mjs`, `test/e2e-sandbox.test.mjs`, `test/policy.test.mjs`, `test/client-dom.test.mjs`, `test/traffic-badge.test.mjs` and `test/release-downloads.test.mjs`. `client-dom.test.mjs` drives the **client half** against a hand-rolled DOM stub (no devDependencies, so CI keeps running plain `node --test`); `policy.test.mjs` covers the tunable-policy layer and the guarded Config builder. Exact case count is reported by the test run — do not hard-code it here beyond the release it was written for.
212
- - Coverage includes rollback, rescue scan/repair, history undo, workspace repoint, post-move verification, recycle bin / backup restore, task center, data-protection cleanup, concurrency locks, error codes, storage-layout self-check, backup orphan GC, tunable policies, and the client half's row/workspace resolution, drag payload, listener teardown and dialog dismissal.
213
- - Adding a test file requires listing it in `package.json` → `scripts.test`; the script enumerates files explicitly, so a new file that is not listed never runs in CI.
214
- - CI: `.github/workflows/test.yml`.
215
- - Release assets may ship a `.tgz` install package (`dsh-workspace-mover-*.tgz`).
216
- - Traffic/clones and release-download badges are refreshed by `.github/workflows/traffic-badge.yml` via Gist JSON endpoints (not part of the runtime plugin).
217
-
218
- ## Related docs
219
-
220
- - User install/usage: [README.md](../README.md), [README_EN.md](../README_EN.md)
221
- - Full version history: [CHANGELOG.md](../CHANGELOG.md)
222
- - Security policy: [SECURITY.md](../SECURITY.md)
223
-
224
- Backup restore derives the filename generation from the official header `version` (legacy headers without a generation use `session.jsonl`); it never defaults v4 content to a v3 filename. Portable Bundle manifests read the plugin version from `package.json`.
1
+ # Architecture
2
+
3
+ Technical reference for contributors and advanced users. Product-facing install/usage docs live in [README.md](../README.md) / [README_EN.md](../README_EN.md). This file is the **single source of truth** for implementation details; do not duplicate it into READMEs.
4
+
5
+ ## Plugin mount (DSH 0.1.5)
6
+
7
+ - Package is pure JavaScript source-as-product (no TypeScript, no build step).
8
+ - Host half mounts through a standard `insert` row in [`cordis.patch.yml`](../cordis.patch.yml).
9
+ - Client half is injected via `package.json` → `dsh.client` (`@deepseek-ai/dsh-client-connection`, `@deepseek-ai/dsh-client-ui-slots`, platform `web`).
10
+ - Official bundles are never rewritten; only event listeners and official slots are used.
11
+ - From GitHub, pnpm runs no install scripts, so `allowBuilds` is not required.
12
+
13
+ ## Host / Client split
14
+
15
+ | Half | Entry | Role |
16
+ | --- | --- | --- |
17
+ | Host | `lib/index.js` | RPC, disk archives, workspace registry, backups, trash, history, locks |
18
+ | Client | `client/client.js` | Sidebar drag intercept, multi-select, confirmation UI, rescue panel (settings slot) |
19
+
20
+ Client locates rows by ARIA semantics only:
21
+
22
+ - session rows: `[aria-selected]`
23
+ - workspace title rows: `[aria-expanded]`
24
+
25
+ It never depends on CSS-module hash class names. UI uses official design tokens (`--dsw-alias-*`) for theme adaptation.
26
+
27
+ Only cross-group drops are intercepted; official same-group reorder stays untouched. After a successful move the client actively refreshes and reconciles the official workspace projection; a page reload remains the user-facing fallback.
28
+
29
+ ## RPC channel
30
+
31
+ Single logical channel `/workspace-mover` via `ctx.connection.rpc.handle('/workspace-mover', …)`. No independent REST routes.
32
+
33
+ | Endpoint | Purpose |
34
+ | --- | --- |
35
+ | `mover.status` | Capability report (detected services / degraded items) plus `storageLayout`, `degradedFeatures`, and the `unarchiveApi` / `setTitleApi` flags |
36
+ | `mover.workspaces` | List workspaces |
37
+ | `mover.move` | Single-session true move |
38
+ | `mover.preview` | Read-only migration preview: source/target, collision, running state, adapter and estimated new snapshot bytes; `backup.reused` identifies an existing matching snapshot |
39
+ | `mover.moveMany` | Bulk move (≤ `batchLimit`, default 50) |
40
+ | `mover.scan` | Classified rescue scan (parses the newest `scanLimit`, default 400; reports `scanned` vs `scannedParsed`) |
41
+ | `mover.repair` | Attach / relink / home one item |
42
+ | `mover.repairAll` | Batch repair (per-item isolation) |
43
+ | `mover.history` | Recent moves (≤ `historyLimit`, default 100) |
44
+ | `mover.undo` | Undo one history entry |
45
+ | `mover.ws.audit` | Workspace path health |
46
+ | `mover.ws.create` | Register a new workspace (`name`, `directory`) over an existing folder; `conflict` when the name or the folder is already in use |
47
+ | `mover.ws.rename` | Rename one workspace (`workspaceId`, `name`, optional `dryRun`); display title only, `conflict` on a name another workspace holds |
48
+ | `mover.repoint` | Move-home wizard |
49
+ | `mover.archived` / `mover.unarchive` | List / restore archived sessions (`mover.archived` also reports `truncated` / `scannedParsed` / `scannedTotal` / `archivedTotal`) |
50
+ | `mover.openFolder` | Open group directory in OS file manager |
51
+ | `mover.session.delete` | Session → recycle bin; optional `purgeBackups: true` also deletes that session's migration backups |
52
+ | `mover.trash.list` / `mover.trash.restore` / `mover.trash.purge` | Recycle bin; `purge` accepts optional `purgeBackups: true` |
53
+ | `mover.backups.list` / `mover.backups.restore` / `mover.backups.deleteOne` | Recovery-snapshot management; `list` reports `orphan` per group, physical `count`, plus `orphanGroups` / `orphanBytes` |
54
+ | `mover.tasks.list` / `mover.tasks.retry` / `mover.tasks.forget` | Migration task center |
55
+ | `mover.migrations.enqueue` / `mover.migrations.list` / `mover.migrations.retry` / `mover.migrations.cancel` | Persistent busy-session queue; `retry` accepts one `taskId` or a `taskIds` batch, with per-item failure isolation |
56
+ | `mover.bundle.export` / `mover.bundle.inspect` / `mover.bundle.import` | Portable Bundle export, dry-run inspection, and import through the adapter/backup pipeline |
57
+ | `mover.data.cleanup` | Dry-run and clean aged plugin data (default age = `cleanupDays`) |
58
+
59
+ ### Tunable policies
60
+
61
+ `mover.data.cleanup`'s age and the caps above come from `resolvePolicy()` in `lib/index.js`, read **at the point of use** (never frozen at module load):
62
+
63
+ | Policy | Default | Environment override |
64
+ | --- | --- | --- |
65
+ | physical snapshot retention per session | 20 | `DSH_WORKSPACE_MOVER_BACKUP_KEEP` |
66
+ | move history length | 100 | `DSH_WORKSPACE_MOVER_HISTORY_LIMIT` |
67
+ | batch cap (move / repair) | 50 | `DSH_WORKSPACE_MOVER_BATCH_LIMIT` |
68
+ | repoint cap | 200 | `DSH_WORKSPACE_MOVER_REPOINT_LIMIT` |
69
+ | rescue scan cap | 400 | `DSH_WORKSPACE_MOVER_SCAN_LIMIT` |
70
+ | cleanup age (days) | 30 | `DSH_WORKSPACE_MOVER_CLEANUP_DAYS` |
71
+
72
+ An unparseable or out-of-range value falls back to the default, so a bad configuration can never disable a feature.
73
+
74
+ ### Dependency declaration, and why every peer is `*`
75
+
76
+ `package.json` declares the eight host packages this plugin integrates with as `peerDependencies`, all `*` and all `optional` in `peerDependenciesMeta`. They are **declarative metadata for storefronts**, not a compatibility gate: the plugin fail-softs at runtime and reports any real degradation through `mover.status` capabilities and `mover_doctor`. The real floor is `engines.dsh` (`>=0.1.5-rc.1`) plus `engines.node`.
77
+
78
+ This was not the first attempt. The ranges originally used the ecosystem's "explicit prerelease branch" idiom, e.g. `>=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0`, and the host then reported the plugin as **incompatible with `0.2.0-rc.2`** — its own tested host line. The mechanism, confirmed by running dsh-market's own range evaluator:
79
+
80
+ - npm's prerelease rule is evaluated at the **comparator-set level**: one `||` alternative is one set, and a prerelease version satisfies that set only when at least one comparator in it shares the version's exact `[major, minor, patch]` tuple *and* carries a prerelease tag; only then are all comparators checked normally.
81
+ - `<0.2.0-0` does share the `0.2.0` tuple and does carry a prerelease, so the set is admitted — but `0.2.0-rc.2 > 0.2.0-0` (the `-0` sentinel sorts below every real prerelease), so the bound then excludes **every** 0.2.0 prerelease.
82
+
83
+ Enumerating release lines reproduces the fault on each new line: a `<0.3.0-0` bound breaks the moment a 0.3 prerelease ships. Hence `*`. dsh-market filters peer declarations to `/^@deepseek-ai\/dsh(?:-|$)/`, which is also why `@deepseek-ai/cordis` and `@deepseek-ai/schemastery` were never named in the warning — they are only evaluated when `engines.dsh` mentions them, and it does not.
84
+
85
+ `test/policy.test.mjs` asserts that the declared ranges plus `engines.dsh` derive `compatible` — never `incompatible` — for DSH versions from `0.1.5-rc.1` through `1.0.0`.
86
+
87
+ **Official Config is deliberately not registered.** The schema would need `@deepseek-ai/schemastery`, which the host loader provides but which is not resolvable from this plugin's directory (verified by `createRequire` from `lib/index.js`); the npm `schemastery` has no `.volatile()` either. Because a bundle-layer static import failure breaks **boot** (a documented ecosystem failure class), `lib/config.js` never imports it — it exposes `buildConfig(z)` / `attachConfig(moduleOrBuilder)` so a host that hands over a schema builder can attach one, and exports `Config === undefined` otherwise. `buildConfig` contains all its own failures and returns `undefined` rather than throwing.
88
+
89
+ ### Official-API preference is observable
90
+
91
+ v2.2 prefers two documented host APIs and keeps an equivalent fallback for each: `registry.unarchiveSession()` (fallback: `enqueueOperation` + `setState`) and `entity.setTitle()` (formerly folded into `entity.mutate`). Both fallbacks are silent by design — the feature still works — so `mover.status.capabilities` reports `unarchiveApi` / `setTitleApi` and lists `unarchive-official-api` / `set-title-official-api` in `degradedFeatures` when the host lacks them. That is what makes "which path did it take?" answerable on a real machine instead of a guess.
92
+
93
+ Errors carry stable codes (`busy`, `conflict`, `not-found`, `rollback-failed`, …). Host logs write failures under `MOVE FAILED`.
94
+
95
+ ## Agent tools (DSH ≥ 0.1.5 with `dsh-tools`)
96
+
97
+ When the host provides the `tools` service, `apply()` registers six plain (zero-dependency) tool definitions; each tool's execute re-enters the internal RPC dispatch, so agent calls share the panel's locks, error codes, and transaction semantics:
98
+
99
+ | Tool | Gate | Purpose |
100
+ | --- | --- | --- |
101
+ | `mover_list_sessions` | none (read-only) | Compose scan + workspace list (sessions capped at 40, counts, ghosts, recoveryCount) for id resolution |
102
+ | `mover_move_session` | user approval | Move one session (`sessionId`, `targetWorkspaceId`, optional `sessionTitle`) |
103
+ | `mover_repair_sessions` | user approval | One `mover.repairAll` pass (accounting-only) |
104
+ | `mover_doctor` | none (read-only) | One `mover.doctor` self-check pass (host services, data dirs, recovery records, workspace paths) |
105
+ | `mover_create_workspace` | none | Register a workspace (`name`, `directory`) through `mover.ws.create` |
106
+ | `mover_rename_workspace` | user approval unless full access | Rename a workspace (`workspaceId`, `name`) through `mover.ws.rename` |
107
+
108
+ - Mutating tools call `approval.request({ agent, toolName, callId, reason, signal })` before acting; `allowed-once` proceeds, anything else (`denied` / `cancelled` / `unavailable`) or a throwing seam fails closed. Hosts without an approval seam refuse operations requiring approval and direct the user to the rescue panel. `callId` is the tool call's own id. The ACP answerer delegates any request without one, so ACP sessions could never approve before it was passed, and the Web prompt uses it to attach to the tool call it already shows.
109
+ - `mover_rename_workspace` skips the prompt when the calling session's effective sandbox mode, read from `ctx.sandboxPolicy.resolve({ session })`, is `danger-full-access` — the same test the official `plugin_manager` tool uses. The bypass is required, not only convenient: the shipped `danger-full-access` preset pairs that mode with approval policy `never`, under which every request resolves `rejected`. A missing policy service or a throwing `resolve` counts as *not* full access, so the tool asks. The tool validates through `dryRun` first, so an unknown id, a blank name, or a name conflict fails before the user is asked; renaming to the current title is a no-op that never prompts. The commit re-validates inside the serialized naming queue, so a conflict created while the prompt was open is still rejected. `mover_doctor` reports whether full-access detection is available under `host-sandbox-policy`.
110
+ - `mover_create_workspace` is ungated because it only adds a registry entry for an existing folder: it never creates folders or files, and a directory already used by a workspace is rejected. "Same folder" uses the host's own identity. The official `registry.create()` canonicalizes with `realpath` and returns the *existing* entity for an owned path, so a trailing slash, `..`, or a symlink alias counts as the same folder. The plugin treats "returned an entity that existed before the call" as `conflict` instead of re-implementing path comparison.
111
+ - Workspace names must be unique (exact match after trimming) for both create and rename. The host registry allows duplicate titles, but the official rename dialog refuses them; the plugin applies that rule to both tools. Check and write run in one serialized queue so two calls cannot claim the same name.
112
+ - `exec.signal` propagates into the RPC layer; cancellation lets the in-flight step finish rolling back to a consistent state.
113
+ - Hosts without `dsh-tools` skip registration silently; `mover.status` reports the `agentTools` flag.
114
+ - **Only `workspaceRegistry` and `sessionPersistence` are in `inject`.** Cordis treats every `inject` entry as required: a missing one leaves the whole plugin fiber inactive, which removes the rescue panel's RPC channel along with every tool. `tools` and `approval` were listed until 2.3.x, so a host without `dsh-user-approval` lost the plugin entirely, and the old missing-approval branch could never run. Now the tools register inside a nested `ctx.inject(['tools'], …)`, so they appear and disappear with `dsh-tools` and the `agentTools` flag follows. Missing approval now blocks operations that require confirmation. The approval service is read with `ctx.get('approval')` on each call, so installing or removing `dsh-user-approval` takes effect without re-mounting the plugin.
115
+
116
+ ## Session archives and storage adapter
117
+
118
+ - The fallback filesystem adapter recognizes names `session.jsonl[.zstd]` and `session.vN.jsonl[.zstd]`; it selects the highest generation without rewriting older files to v3.
119
+ - A mixed plain/Zstd directory and a file whose compression differs from the host's configured mode are rejected explicitly. Mixed compression elsewhere under the same root is diagnosed per session, not by a root-wide scan.
120
+ - v3/v4 filenames are recognized, but a v4 runtime fixture has not yet been verified. Filename/header mismatch is rejected only when the header contains one of the generation fields currently recognized by the adapter.
121
+ - True move keeps the session id and all frames; only the header `cwd` (first frame) is rewritten; other frames stay byte-identical. Publish via temp file + atomic rename.
122
+
123
+ ### The storage-layout assumption, and its self-check
124
+
125
+ The adapter first reports the public `list` / `stat` / `open` surface. When a provider also exposes a physical root, the plugin uses the legacy directory adapter as a visible `filesystem-fallback`; when only official handles are available it stays read/diagnostic capable and marks physical moves as `unsupported` instead of guessing a path.
126
+
127
+ `verifyStorageLayout(ctx)` closes that gap: it takes up to five real headers from `persistence.list()`, reverse-derives each session's directory with this plugin's own encoding, and confirms an archive is actually there. Result states are `ok` / `degraded` / `empty` (no sessions to verify) / `unavailable`. It is exposed as `mover.status.capabilities.storageLayout` plus a `data-layout` check in `mover_doctor`, is strictly read-only, and never throws (any error degrades to a warning).
128
+
129
+ `mover.status.capabilities.storageAdapter` is `filesystem-fallback`, `official-handle`, or `unavailable`; `storageApi`, `physicalPath`, and `migrationQueueState` expose the exact degradation and queue state. `mover_doctor` reports the same adapter under `storage-adapter` and checks the durable migration records under `data-migrations`.
130
+
131
+ Portable Bundles contain `manifest.json`, `sessions/`, `attachments/`, `checksums.json`, and `workspace-metadata.json`. Import defaults to dry-run and reports id conflicts, missing attachments, compression incompatibilities, required/free space, and recoverable files. Execution refuses a blocked plan, backs up each artifact, verifies the rewritten header, and isolates failures per session. Attachment files are preserved and copied into the imported session directory; the bundle report calls out any missing file.
132
+
133
+ ## Move pipeline
134
+
135
+ Order of operations for a cross-workspace move:
136
+
137
+ 1. **Running check** — queue running or resident sessions; a resident backend writer cannot be safely rebound through the current public API. Await backend `flush()` when available, then recheck residency before reading the archive.
138
+ 2. **Read authority header** from disk; verify target ≠ source.
139
+ 3. **Recovery snapshot** to `$DSH_HOME/workspace-mover/backups/` (up to `backupKeep` distinct physical files per session, default 20) **before any side effect**. The content fingerprint ignores the migration-only header `cwd`, so a move back and forth reuses the existing snapshot. A backup failure aborts with `backup-failed` and nothing has been touched.
140
+ 4. **Preflight** — dual-accounting detect, target writability probe, disk-space hint.
141
+ 5. **Rewrite header cwd**; if an existing `working-directory/change` would override it, append a new event with the next contiguous sequence number. Preserve original events. Reject unsupported generations or invalid sequences before detach.
142
+ 6. **Move directory** (Windows: exponential-backoff retry on EPERM; fallback copy+delete).
143
+ 7. **In-memory closeout** — invalidate three registry indexes before official `attachSession`; never change private writer state or live session headers. Clear the cached `workingDirectory` row so it rebuilds from the migrated log.
144
+ 8. **Accounting** — source `detachSession` then target `attachSession`.
145
+ 9. **Verify** — read archive back; id + cwd must match or the move fails.
146
+ 10. **Rollback on any failure** — restore original bytes back to source dir and reattach the source workspace.
147
+
148
+ Concurrency: same session / same target workspace try-acquire a lock. A running or resident session returns `busy` to the transaction layer and is persisted in `migrations.json` as `waiting-for-idle`; the queue wakes every few seconds and on the next plugin start. Idle alone does not release a resident writer: wait for unloading or restart. Workspace repoint checks residency before changing the workspace path. Bulk moves de-duplicate first and isolate each result.
149
+
150
+ If “rollback also failed”, a durable recovery record is written and shown as a red manual-recovery row. Session files and backups stay in place — never auto-deleted.
151
+
152
+ ## Workspace repoint (move-home)
153
+
154
+ Official workspace entity writes prune members using in-memory session cwd indexes. The wizard:
155
+
156
+ 1. Audits path validity (`mover.ws.audit`).
157
+ 2. Pre-seeds all affected sessions’ three indexes to the new path.
158
+ 3. Rewrites the entity path through the unified `mutate` channel so members are not pruned.
159
+ 4. Batch-migrates member sessions and strays from the old path through the same move pipeline.
160
+ 5. Running sessions skip; an interrupted run resumes with only the remainder.
161
+
162
+ If `mutate` is unavailable after a host upgrade, the wizard aborts before the first file change.
163
+
164
+ ## Plugin-owned data
165
+
166
+ All plugin metadata lives under `$DSH_HOME/workspace-mover/` and never overwrites official session archives in place except through the move pipeline.
167
+
168
+ | Path | Use |
169
+ | --- | --- |
170
+ | `backups/` | Byte-level recovery snapshots; new files include a SHA-256 content fingerprint, while legacy timestamp-only names remain readable |
171
+ | `history.json` | Last 100 cross-workspace moves |
172
+ | trash / task / recovery files | Recycle bin manifest, task center, recovery ledger |
173
+
174
+ Official session files and workspace registry are only mutated via host APIs: registry `mutate` / `attachSession` / `detachSession` and durable domain state.
175
+
176
+ ## Compatibility boundary
177
+
178
+ Verified (each entry dated; the newest verification supersedes the badge claims in the READMEs):
179
+
180
+ - DeepSeek Harness `0.2.0-rc.2` (desktop, 2026-10-04) — see `docs/compatibility/dsh-0.2.0-rc.2-2026-10-04.md`
181
+ - DeepSeek Harness `0.1.5-rc.1` / `0.1.5-rc.2` (earlier release line)
182
+ - dsh-market `1.45.1`
183
+ - Node.js `≥ 22`
184
+ - `package.json` → `engines.dsh`: `>= 0.1.5-rc.1` (a lower bound only; it is deliberately not an upper bound, and dsh-market reads it for its host-requirement card)
185
+
186
+ GitHub-only packages may show “host requirement unknown” in dsh-market when no npm manifest is published; that is metadata, not runtime incompatibility.
187
+
188
+ Version-sensitive host behaviors degrade with explicit UI messages (not silent failure):
189
+
190
+ - Unarchive needs the registry durable state channel.
191
+ - Projection-cache titles parse defensively against v3; missing file falls back to archive header title.
192
+ - Host upgrades that rename registry cache fields hit the degrade path (feature still usable; ownership refresh may need a restart).
193
+
194
+ ## Coexistence notes (implementation)
195
+
196
+ - CSS classes `wsm-*` and DOM attributes `data-wsm-*` are private namespaces.
197
+ - Settings panel uses the official `settings.section` slot.
198
+ - Sidebar redraw plugins that replace workspace DOM can break ARIA row lookup — features stop triggering; data is not damaged.
199
+ - Do not install a second document-level drag interceptor for cross-workspace moves.
200
+
201
+ ## Client-half invariants
202
+
203
+ - **Every document-level listener and the menu `MutationObserver` is registered through the `listen()` helper and released in one `ctx.effect` teardown**, and `apply` is guarded by a module-level `applied` flag. The desktop client disposes and recomposes the plugin fiber on hot reload; without both, a second copy of every listener stacks up (duplicate confirm dialogs, duplicate RPC, two closures fighting over the same selection highlight).
204
+ - **A row is only ever mapped to a session id by a verifiable credential.** The authoritative path walks the React fiber to `memoizedProps.node.id`; the fallback aligns DOM rows to `workspace.sessionIds` by exact/longest title match. A row that cannot be confidently mapped is reported, never substituted — the previous "take the next id in order" behavior could move a different, valid session that the host cannot detect.
205
+ - **A workspace header is only mapped to a registry entity when the header text contains that entity's title or path** (or when no entity carries a display identity at all). `registry.list()` includes workspaces whose folder is gone while the sidebar hides them, so positional mapping is unsafe.
206
+ - `rowSessionId`'s fiber guard is intentionally permissive; if the official node shape changes, the fallback engages and the user is told, rather than the feature silently doing nothing.
207
+ - Dialogs are built by `injectOverlay()` and handed to `openDialog()`, which appends **before** focusing (an element not in the document cannot take focus in a browser), focuses the primary button, and traps Tab within the card.
208
+
209
+ ## Tests & release
210
+
211
+ - `npm test` runs `test/core.test.mjs`, `test/e2e-sandbox.test.mjs`, `test/policy.test.mjs`, `test/client-dom.test.mjs`, `test/traffic-badge.test.mjs` and `test/release-downloads.test.mjs`. `client-dom.test.mjs` drives the **client half** against a hand-rolled DOM stub (no devDependencies, so CI keeps running plain `node --test`); `policy.test.mjs` covers the tunable-policy layer and the guarded Config builder. Exact case count is reported by the test run — do not hard-code it here beyond the release it was written for.
212
+ - Coverage includes rollback, rescue scan/repair, history undo, workspace repoint, post-move verification, recycle bin / backup restore, task center, data-protection cleanup, concurrency locks, error codes, storage-layout self-check, backup orphan GC, tunable policies, and the client half's row/workspace resolution, drag payload, listener teardown and dialog dismissal.
213
+ - Adding a test file requires listing it in `package.json` → `scripts.test`; the script enumerates files explicitly, so a new file that is not listed never runs in CI.
214
+ - CI: `.github/workflows/test.yml`.
215
+ - Release assets may ship a `.tgz` install package (`dsh-workspace-mover-*.tgz`).
216
+ - Traffic/clones and release-download badges are refreshed by `.github/workflows/traffic-badge.yml` via Gist JSON endpoints (not part of the runtime plugin).
217
+
218
+ ## Related docs
219
+
220
+ - User install/usage: [README.md](../README.md), [README_EN.md](../README_EN.md)
221
+ - Full version history: [CHANGELOG.md](../CHANGELOG.md)
222
+ - Security policy: [SECURITY.md](../SECURITY.md)
223
+
224
+ Backup restore derives the filename generation from the official header `version` (legacy headers without a generation use `session.jsonl`); it never defaults v4 content to a v3 filename. Portable Bundle manifests read the plugin version from `package.json`.
@@ -1,45 +1,45 @@
1
- # DSH compatibility report: 0.2.0-rc.2 · 2026-10-13
2
-
3
- Release: 2.3.0. Desktop results below are user-reported partial verification; they do not claim a full real-runtime smoke pass.
4
-
5
- ## Matrix
6
-
7
- | Environment | Node | Result | Scope |
8
- | --- | --- | --- | --- |
9
- | DSH Desktop 0.2.0-rc.2 | pending | partial: user confirmed idle-session migration and snapshot reuse; initial return-preview failures and corrections are documented below | adapter status, v4 move, preview; queue resume still pending |
10
- | DSH Web 0.2.0-rc.2 | pending | pending user verification | same smoke flow on web connection |
11
- | Local sandbox fixtures | 22.23.2 | passed: `npm run check`, 171/171 tests, `npm pack --dry-run` | v3/v4 Zstd, plain JSONL, format diagnostics, rollback, queue state transitions, return preview, drop-target identity, explicit target selection, snapshot deduplication |
12
- | Node 24 fixtures | 24 | not run | CI/runtime matrix remains pending |
13
-
14
- ## Implemented checks
15
-
16
- - `filesystem-fallback`, `official-handle`, and `unavailable` adapter modes are reported through `mover.status` and `mover_doctor`.
17
- - Physical operations fail with a stable `unsupported` error when the provider exposes no raw-byte path.
18
- - A running session is persisted in `migrations.json` as `waiting-for-idle`; the queue drains after idle and after plugin re-mount. Process-restart validation remains pending.
19
- - `mover.preview` reports source, target, collision, running state, adapter mode, warnings, and estimated new snapshot bytes; a matching snapshot is reported as reused.
20
- - Recovery snapshots use a SHA-256 fingerprint of session content with migration-only `cwd` normalized. Legacy timestamp-only backup names remain readable. Each migration still records its source, target, and snapshot reference in history.
21
- - Mixed compression is rejected explicitly; no format upgrade is performed. Header-generation mismatch detection only applies when the header declares a recognized generation field.
22
- - The local queue test covers plugin re-mount. A full DSH process restart with a waiting task has not been exercised yet.
23
- - The user confirmed snapshot reuse for an old idle session after moving it; local fixtures cover Zstd, plain JSONL, round trips, and content changes.
24
-
25
- ## Still requiring a real DSH run
26
-
27
- - Official-runtime plain JSONL and v3/v4 fixtures, including mismatch and mixed-layout diagnostics. The local sandbox fixtures are not official DSH runtime evidence.
28
- - Desktop and Web smoke flow with a real session: create, preview, move, verify id/cwd/events, conflict, attach failure and byte-exact rollback.
29
- - Restart DSH while a queue item is waiting, then verify it completes after the session becomes idle.
30
-
31
- The next report should replace the pending rows with the exact DSH build, Node version, successful scenarios, and known limits.
32
-
33
- ## Desktop feedback and correction
34
-
35
- The user restarted Desktop and reported all doctor checks passing except the expected filesystem adapter warning. A session moved from `E:\DeepSeekHarness` to `E:\VSCode_Project\quant_research`. Read-only inspection found one `session.v4.jsonl.zstd` primary archive at the destination (419,562 bytes) and no primary archive for that ID at the source. The return preview nevertheless showed both source and target as `quant_research` and incorrectly labelled the session's own archive as a conflict.
36
-
37
- The candidate now resolves drop targets from the rendered `data-row-key` workspace ID before React group props and unique text evidence. It refreshes workspace data at drop time and rejects ambiguous text matches. Same-workspace previews distinguish existing membership from an unregistered archive, and report no recovery backup for those operations. Doctor adapter explanations and preview notices are localized; the recovery backup copy is explicitly described as separate from the registered primary session.
38
-
39
- The user retested after restart and still received a same-workspace return preview. The exact Desktop event target has not been captured, so the identity-based correction alone is not considered verified. Further changes remove the stale last-hover fallback when dropping outside a header and clear the hover target when leaving a header. Single-session confirmation now offers an explicit workspace selector, refreshes the preview for the selected target and fork scope, and keeps execution disabled while loading or on preview failure. Late preview responses are ignored. These flows have DOM regression coverage; The latest user feedback confirms migration and snapshot reuse, but does not separately document every selector/drag scenario.
40
-
41
- ## Release checks
42
-
43
- - Version 2.3.0: local Node 22.23.2 checks passed, 171/171 tests, syntax checks and npm pack dry-run.
44
- - Regression coverage now includes newest/default versus explicitly selected backup restoration, attachment space estimates, and insufficient-space import refusal before writing.
45
- - README_EN.md and CHANGELOG.md are included in the npm package.
1
+ # DSH compatibility report: 0.2.0-rc.2 · 2026-10-13
2
+
3
+ Release: 2.3.0. Desktop results below are user-reported partial verification; they do not claim a full real-runtime smoke pass.
4
+
5
+ ## Matrix
6
+
7
+ | Environment | Node | Result | Scope |
8
+ | --- | --- | --- | --- |
9
+ | DSH Desktop 0.2.0-rc.2 | pending | partial: user confirmed idle-session migration and snapshot reuse; initial return-preview failures and corrections are documented below | adapter status, v4 move, preview; queue resume still pending |
10
+ | DSH Web 0.2.0-rc.2 | pending | pending user verification | same smoke flow on web connection |
11
+ | Local sandbox fixtures | 22.23.2 | passed: `npm run check`, 171/171 tests, `npm pack --dry-run` | v3/v4 Zstd, plain JSONL, format diagnostics, rollback, queue state transitions, return preview, drop-target identity, explicit target selection, snapshot deduplication |
12
+ | Node 24 fixtures | 24 | not run | CI/runtime matrix remains pending |
13
+
14
+ ## Implemented checks
15
+
16
+ - `filesystem-fallback`, `official-handle`, and `unavailable` adapter modes are reported through `mover.status` and `mover_doctor`.
17
+ - Physical operations fail with a stable `unsupported` error when the provider exposes no raw-byte path.
18
+ - A running session is persisted in `migrations.json` as `waiting-for-idle`; the queue drains after idle and after plugin re-mount. Process-restart validation remains pending.
19
+ - `mover.preview` reports source, target, collision, running state, adapter mode, warnings, and estimated new snapshot bytes; a matching snapshot is reported as reused.
20
+ - Recovery snapshots use a SHA-256 fingerprint of session content with migration-only `cwd` normalized. Legacy timestamp-only backup names remain readable. Each migration still records its source, target, and snapshot reference in history.
21
+ - Mixed compression is rejected explicitly; no format upgrade is performed. Header-generation mismatch detection only applies when the header declares a recognized generation field.
22
+ - The local queue test covers plugin re-mount. A full DSH process restart with a waiting task has not been exercised yet.
23
+ - The user confirmed snapshot reuse for an old idle session after moving it; local fixtures cover Zstd, plain JSONL, round trips, and content changes.
24
+
25
+ ## Still requiring a real DSH run
26
+
27
+ - Official-runtime plain JSONL and v3/v4 fixtures, including mismatch and mixed-layout diagnostics. The local sandbox fixtures are not official DSH runtime evidence.
28
+ - Desktop and Web smoke flow with a real session: create, preview, move, verify id/cwd/events, conflict, attach failure and byte-exact rollback.
29
+ - Restart DSH while a queue item is waiting, then verify it completes after the session becomes idle.
30
+
31
+ The next report should replace the pending rows with the exact DSH build, Node version, successful scenarios, and known limits.
32
+
33
+ ## Desktop feedback and correction
34
+
35
+ The user restarted Desktop and reported all doctor checks passing except the expected filesystem adapter warning. A session moved from `E:\DeepSeekHarness` to `E:\VSCode_Project\quant_research`. Read-only inspection found one `session.v4.jsonl.zstd` primary archive at the destination (419,562 bytes) and no primary archive for that ID at the source. The return preview nevertheless showed both source and target as `quant_research` and incorrectly labelled the session's own archive as a conflict.
36
+
37
+ The candidate now resolves drop targets from the rendered `data-row-key` workspace ID before React group props and unique text evidence. It refreshes workspace data at drop time and rejects ambiguous text matches. Same-workspace previews distinguish existing membership from an unregistered archive, and report no recovery backup for those operations. Doctor adapter explanations and preview notices are localized; the recovery backup copy is explicitly described as separate from the registered primary session.
38
+
39
+ The user retested after restart and still received a same-workspace return preview. The exact Desktop event target has not been captured, so the identity-based correction alone is not considered verified. Further changes remove the stale last-hover fallback when dropping outside a header and clear the hover target when leaving a header. Single-session confirmation now offers an explicit workspace selector, refreshes the preview for the selected target and fork scope, and keeps execution disabled while loading or on preview failure. Late preview responses are ignored. These flows have DOM regression coverage; The latest user feedback confirms migration and snapshot reuse, but does not separately document every selector/drag scenario.
40
+
41
+ ## Release checks
42
+
43
+ - Version 2.3.0: local Node 22.23.2 checks passed, 171/171 tests, syntax checks and npm pack dry-run.
44
+ - Regression coverage now includes newest/default versus explicitly selected backup restoration, attachment space estimates, and insufficient-space import refusal before writing.
45
+ - README_EN.md and CHANGELOG.md are included in the npm package.