dsh-session-manager 0.7.3 → 0.7.4
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 +11 -0
- package/README.md +3 -3
- package/README.zh.md +3 -3
- package/lib/client.js +1 -1
- package/lib/compat/dsh-adapter.js +36 -2
- package/lib/index.js +362 -47
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## 0.7.4 — 2026-10-20
|
|
6
|
+
|
|
7
|
+
- **fix(workspace)**: Repair workspace session membership on DSH 0.2.x. New and forked sessions are now attributed to the workspace owning their `cwd` as soon as the session is announced (runtime repair, no restart needed), and historical sessions whose `cwd` matches a registered workspace are re-registered on startup — all through the official DSH workspace entity API (#41).
|
|
8
|
+
- **fix(move)**: "Move to workspace" on a session whose `cwd` already equals the target workspace path now repairs the missing membership instead of returning a silent no-op. The response reports whether a membership was written (`attached`); the artifact's content, size and mtime are untouched and running sessions are never restarted (#41).
|
|
9
|
+
- **fix(compat)**: Resolve the DSH home through the official `$DSH_HOME` → `~/.dsh` order (tilde-expanded, absolute) when the `dshHomePath` service is not visible to the plugin scope, so session scans and stores work with a custom DSH home (#41).
|
|
10
|
+
- **fix(workspace)**: Read durable workspace membership from the registry record instead of the runtime-filtered projection when deleting sessions, so stale runtime indexes can no longer leave dead entries behind.
|
|
11
|
+
- **test**: Add regression coverage for runtime new/forked session attribution, boot repair, same-path move repair (including artifact byte/size/mtime invariants), idempotence, cross-workspace moves, cwd mismatch safety, incomplete-scan safety, restart stability, archived-session preservation, and custom DSH home resolution (#41).
|
|
12
|
+
- **compat**: Compatible with DSH Desktop v0.2.1-rc.2 (and v0.2.0-rc.2); workspace membership repair uses the official workspace registry entity API.
|
|
13
|
+
- **fix(safety)**: Workspace membership repair never migrates a session that is already accounted by another workspace: such a conflict is refused with `409 workspace-membership-conflict` and both records stay untouched, because an account removed from a different workspace cannot be restored through the official API. Completing a missing membership still goes through the official `attachSession()` after its validation conditions (header readability, cwd resolution, canonical path equality) are pre-checked, so a predictable rejection fails before any write (#41).
|
|
14
|
+
- **fix(api)**: "Move to workspace" no longer reports success when a repair cannot be completed: hosts without the official `attachSession` API answer `501 workspace-attach-unavailable`, an unconfirmable membership state is refused, a membership conflict answers `409 workspace-membership-conflict`, and unsafe or failed repairs answer `500` with `workspace-repair-unsafe` / `workspace-repair-failed`. Successful repairs report `membership` (`already-member` / `attached`) next to the existing `moved` / `attached` fields (#41).
|
|
15
|
+
|
|
5
16
|
## 0.7.3 — 2026-10-18
|
|
6
17
|
|
|
7
18
|
- **fix(update)**: Fix built-in one-click update and resolve `cannot get property "remote" without inject` by routing update requests through the plugin's own Host API instead of client-side `remote.pluginManager`.
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Verified with the official DSH Desktop client; see [5 Compatibility](#5-compatib
|
|
|
20
20
|
|
|
21
21
|
- **Delete** is irreversible and always asks for confirmation. Subagent sessions and transient blank placeholders (no persisted artifact) cannot be deleted.
|
|
22
22
|
- **Archive / Unarchive** moves a session in and out of the active list without touching its disk content.
|
|
23
|
-
- **Move to workspace** keeps history, title, archive state and derived-session relationships intact, rewrites the session's `cwd` to the target workspace, and updates the live writer's header in place so any pending tool calls keep landing on the new path.
|
|
23
|
+
- **Move to workspace** keeps history, title, archive state and derived-session relationships intact, rewrites the session's `cwd` to the target workspace, and updates the live writer's header in place so any pending tool calls keep landing on the new path. When the session's `cwd` already equals the target workspace path but its membership was never recorded, only the missing membership is repaired: no `cwd` rewrite, no artifact rewrite or move, and no session restart.
|
|
24
24
|
- **Migrate Agent preset** rewrites the latest `agent-preset/selected` event (or the session header if no such event exists) so a session whose preset was renamed or removed can resume. Message history is never altered.
|
|
25
25
|
|
|
26
26
|
### 1.2 Session shortcuts
|
|
@@ -136,7 +136,7 @@ After changing plugin dependencies, `package.json`, bundle configuration, or oth
|
|
|
136
136
|
- Cold rewrites preserve the artifact's stored format (V1 / V2 / V3 / V4 are all readable; the plugin never forces an upgrade). Moves and rewrites refuse corrupt / truncated Zstd logs or JSONL logs with incomplete final lines instead of publishing partial history.
|
|
137
137
|
- Preset migration separates backup, publication and rollback. If rollback fails, recovery files are retained and their paths are included in the error; do not remove them.
|
|
138
138
|
- **Workspace archiving rollback protection**: archiving is refused if active sessions are present, and batch session archiving automatically rolls back on partial failures to preserve state consistency.
|
|
139
|
-
-
|
|
139
|
+
- On startup, sessions whose `cwd` names a registered workspace but whose membership record is missing are re-registered through the official DSH API (idempotent). Incomplete scans skip pruning only: existing membership is never dropped, and live sessions or memberships added during the scan are always preserved.
|
|
140
140
|
- Plugin mutations are serialized per session and request bodies are limited to 64 KiB. This queue supplements, rather than replaces, DSH's own persistence coordination.
|
|
141
141
|
|
|
142
142
|
## 5 Compatibility
|
|
@@ -147,7 +147,7 @@ The table below lists verified version combinations between DeepSeek Harness (DS
|
|
|
147
147
|
|
|
148
148
|
| DeepSeek Harness (DSH) | dsh-session-manager | Verified Environment |
|
|
149
149
|
| :--- | :--- | :--- |
|
|
150
|
-
| **v0.2.1-rc.2** | `0.7.3`, `0.7.2` | Desktop |
|
|
150
|
+
| **v0.2.1-rc.2** | `0.7.4`, `0.7.3`, `0.7.2` | Desktop |
|
|
151
151
|
| **v0.2.0-rc.2** | `0.7.1`, `0.7.0`, `0.6.4`, `0.6.3`, `0.6.2`, `0.5.4` | Desktop |
|
|
152
152
|
| **v0.1.7-rc.2** | `0.5.3` | Desktop / Web UI |
|
|
153
153
|
| **v0.1.7-rc.1** | `0.5.2` | Desktop / Web UI |
|
package/README.zh.md
CHANGED
|
@@ -20,7 +20,7 @@ DSH 会话与工作区管理——工作区:归档与显示。会话:跨工
|
|
|
20
20
|
|
|
21
21
|
- **删除**:不可逆操作,UI 始终要求二次确认。subagent 会话和尚未落盘的空白会话占位不能删除。
|
|
22
22
|
- **归档 / 移出归档**:把会话移出或移回主列表,不删除磁盘内容。
|
|
23
|
-
- **移动至工作区**:跨工作区移动时保留历史、标题、归档状态和派生会话关系,同时把 `cwd` 重写为目标工作区,并就地更新 live writer 的 header
|
|
23
|
+
- **移动至工作区**:跨工作区移动时保留历史、标题、归档状态和派生会话关系,同时把 `cwd` 重写为目标工作区,并就地更新 live writer 的 header,使挂起的工具调用继续落到新路径。若会话的 `cwd` 本来就等于目标工作区、只是归属未登记,则只补全归属记录,不改写 `cwd`、不重写或移动工件、不重启会话。
|
|
24
24
|
- **迁移 Agent 预设**:原预设被改名或删除导致会话无法恢复时,可修复该会话。迁移会就地重写最后一条 `agent-preset/selected` 事件(若从未记录则修改会话 header),不改动历史消息。
|
|
25
25
|
|
|
26
26
|
### 1.2 会话快捷管理
|
|
@@ -134,7 +134,7 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
134
134
|
- 冷会话重写保留原工件格式(V1 / V2 / V3 / V4 都可读,绝不强制升级)。损坏或截断的 Zstd 日志、缺少完整尾行的 JSONL 会拒绝移动 / 重写,不会把部分历史当作完整日志保存。
|
|
135
135
|
- 迁移预设按"备份 → 发布 → 回滚"分阶段处理。如果回滚失败,会保留恢复文件并在错误中报告路径;不要删除这些文件。
|
|
136
136
|
- **工作区归档事务保护**:若工作区内存在运行中会话则拒绝归档;批量归档会话若中途遇到异常会自动回滚已变更状态,避免部分成功导致状态不一致。
|
|
137
|
-
-
|
|
137
|
+
- 启动时若发现 `cwd` 对应已注册工作区但归属记录遗漏的会话,会通过 DSH 官方接口补全登记(幂等);扫描不完整时只跳过清理,不会误删现有归属,也不会清除仍在内存中或扫描期间新加入的会话。
|
|
138
138
|
- 同一会话的插件写操作按顺序执行;请求体限制为 64 KiB。该队列不替代 DSH 自身的持久化写入协调。
|
|
139
139
|
|
|
140
140
|
## 5 兼容性
|
|
@@ -145,7 +145,7 @@ dsh plugin --profile web add <本仓库路径>
|
|
|
145
145
|
|
|
146
146
|
| DeepSeek Harness (DSH) | dsh-session-manager | 验证环境 |
|
|
147
147
|
| :--- | :--- | :--- |
|
|
148
|
-
| **v0.2.1-rc.2** | `0.7.3`, `0.7.2` | Desktop |
|
|
148
|
+
| **v0.2.1-rc.2** | `0.7.4`, `0.7.3`, `0.7.2` | Desktop |
|
|
149
149
|
| **v0.2.0-rc.2** | `0.7.1`, `0.7.0`, `0.6.4`, `0.6.3`, `0.6.2`, `0.5.4` | Desktop |
|
|
150
150
|
| **v0.1.7-rc.2** | `0.5.3` | Desktop / Web UI |
|
|
151
151
|
| **v0.1.7-rc.1** | `0.5.2` | Desktop / Web UI |
|
package/lib/client.js
CHANGED
|
@@ -9,10 +9,36 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { createRequire } from "node:module";
|
|
11
11
|
import { existsSync, readFileSync } from "node:fs";
|
|
12
|
-
import {
|
|
12
|
+
import { homedir } from "node:os";
|
|
13
|
+
import { join, resolve } from "node:path";
|
|
13
14
|
|
|
14
15
|
const require = createRequire(import.meta.url);
|
|
15
16
|
|
|
17
|
+
/** Expand a leading `~` / `~/` / `~\` against the OS home, matching
|
|
18
|
+
* `@deepseek-ai/dsh-home-paths` `expandHomePath`. */
|
|
19
|
+
function expandHomePath(path) {
|
|
20
|
+
if (path === "~") return homedir();
|
|
21
|
+
if (path.startsWith("~/") || path.startsWith("~\\")) return join(homedir(), path.slice(2));
|
|
22
|
+
return path;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolve the DSH home without the `dshHomePath` service. DSH 0.2.x resolves
|
|
27
|
+
* the harness home exclusively through `resolveDshHome()`: `$DSH_HOME` when
|
|
28
|
+
* set and non-blank, otherwise `~/.dsh`; the value is tilde-expanded and
|
|
29
|
+
* resolved to an absolute path. Mirror that order so a plugin scope that
|
|
30
|
+
* cannot see the root-context service still lands on the same directory.
|
|
31
|
+
*/
|
|
32
|
+
function fallbackDshHome() {
|
|
33
|
+
const raw = typeof process.env.DSH_HOME === "string" ? process.env.DSH_HOME.trim() : "";
|
|
34
|
+
const selected = raw.length > 0 ? expandHomePath(raw) : join(homedir(), ".dsh");
|
|
35
|
+
return resolve(selected);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Same calling convention as the official `dshHomePath(...segments)`. */
|
|
39
|
+
function fallbackHomePath(...segments) {
|
|
40
|
+
return join(fallbackDshHome(), ...segments);
|
|
41
|
+
}
|
|
16
42
|
/** Pull one service out of the Cordis ctx, tolerating direct property fallbacks. */
|
|
17
43
|
export function getService(ctx, name) {
|
|
18
44
|
try {
|
|
@@ -157,7 +183,15 @@ export function createDshAdapter(ctx) {
|
|
|
157
183
|
get persistence() { return getService(ctx, "sessionPersistence"); },
|
|
158
184
|
get agentPresets() { return getService(ctx, "agentPresets"); },
|
|
159
185
|
get projectionCache() { return getService(ctx, "sessionProjectionCache"); },
|
|
160
|
-
get homePath() {
|
|
186
|
+
get homePath() {
|
|
187
|
+
// DSH 0.2.x provides `dshHomePath` on the root context (app-boot), but a
|
|
188
|
+
// plugin scope may not resolve it; without a fallback every disk-backed
|
|
189
|
+
// path (sessions scan, annotation store, workspace archive store) is
|
|
190
|
+
// lost. Fall back to the official home resolution instead.
|
|
191
|
+
const served = getService(ctx, "dshHomePath");
|
|
192
|
+
if (typeof served === "function") return served;
|
|
193
|
+
return fallbackHomePath;
|
|
194
|
+
},
|
|
161
195
|
get pluginManager() { return getService(ctx, "pluginManager"); },
|
|
162
196
|
|
|
163
197
|
getSession(id) {
|
package/lib/index.js
CHANGED
|
@@ -101,6 +101,12 @@ function rebindLiveWriter(persistence, sessionId, newHeader) {
|
|
|
101
101
|
* disk fallback semantics are preserved -- the orphan is the whole
|
|
102
102
|
* reason this loop exists.
|
|
103
103
|
*
|
|
104
|
+
* The result carries two completeness flags: "persisted" reports whether the
|
|
105
|
+
* persistence index itself listed successfully (its rows are authoritative),
|
|
106
|
+
* while "complete" additionally requires the disk sweep to finish without
|
|
107
|
+
* errors. Callers that only add validated memberships may run on "persisted";
|
|
108
|
+
* callers that remove records must wait for "complete".
|
|
109
|
+
*
|
|
104
110
|
* @param {object|undefined} persistence DSH's sessionPersistence service
|
|
105
111
|
* @param {((segment: string) => string)|undefined} dshHome Resolver for
|
|
106
112
|
* DSH home sub-paths; only "sessions" is read.
|
|
@@ -110,6 +116,7 @@ const scanSessionHeaders = async (persistence, dshHome) => {
|
|
|
110
116
|
const headers = [];
|
|
111
117
|
const errors = [];
|
|
112
118
|
let listed = false;
|
|
119
|
+
let persisted = false;
|
|
113
120
|
if (typeof persistence?.list === "function") {
|
|
114
121
|
try {
|
|
115
122
|
const rows = await persistence.list();
|
|
@@ -122,9 +129,10 @@ const scanSessionHeaders = async (persistence, dshHome) => {
|
|
|
122
129
|
if (!seen.has(header.id)) { seen.add(header.id); headers.push(header); }
|
|
123
130
|
}
|
|
124
131
|
listed = true;
|
|
132
|
+
persisted = true;
|
|
125
133
|
} catch (error) { errors.push(error); }
|
|
126
134
|
}
|
|
127
|
-
const result = () => ({ headers, complete: listed && errors.length === 0, errors });
|
|
135
|
+
const result = () => ({ headers, persisted, complete: listed && errors.length === 0, errors });
|
|
128
136
|
// Skip known files before fopen; retain the orphan/raw-id compatibility path.
|
|
129
137
|
// Only add canonical session directories that do not start and end with "--"
|
|
130
138
|
// to avoid colliding with legacy projectKey candidates (e.g. "--s1--").
|
|
@@ -175,6 +183,16 @@ const scanSessionHeaders = async (persistence, dshHome) => {
|
|
|
175
183
|
|
|
176
184
|
const listSessionHeaders = async (persistence, dshHome) => (await scanSessionHeaders(persistence, dshHome)).headers;
|
|
177
185
|
|
|
186
|
+
/** Canonicalize a directory path the way DSH's workspace registry does
|
|
187
|
+
* (fs.realpath on a fully qualified path). Returns undefined when the path
|
|
188
|
+
* cannot be resolved so callers skip instead of guessing; DSH stores
|
|
189
|
+
* workspace paths canonicalized, so canonical equality is the only safe
|
|
190
|
+
* membership comparison on Windows (case, separators, 8.3 names, junctions). */
|
|
191
|
+
const canonicalizeDirectory = async (path) => {
|
|
192
|
+
if (typeof path !== "string" || path === "") return undefined;
|
|
193
|
+
try { return await realpath(path); } catch { return undefined; }
|
|
194
|
+
};
|
|
195
|
+
|
|
178
196
|
const UPDATE_STATUS_FILE = ".dsh-session-manager-update.json";
|
|
179
197
|
const STALE_EXTERNAL_TASK_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes fallback for dead external processes
|
|
180
198
|
const HEARTBEAT_STALE_TIMEOUT_MS = 3 * 60 * 1000; // 3 minutes without heartbeat for external processes
|
|
@@ -367,7 +385,7 @@ function verifyInstalledPackage(profileDir, expectedVersion) {
|
|
|
367
385
|
|
|
368
386
|
export function apply(ctx) {
|
|
369
387
|
try {
|
|
370
|
-
const buildId = "dsh-session-manager v0.7.
|
|
388
|
+
const buildId = "dsh-session-manager v0.7.4 (workspace-archive)";
|
|
371
389
|
if (typeof ctx?.logger?.info === "function") ctx.logger.info(buildId);
|
|
372
390
|
else if (typeof console !== "undefined" && console?.info) console.info(buildId);
|
|
373
391
|
} catch (_) { /* ignore logging failures */ }
|
|
@@ -399,10 +417,26 @@ export function apply(ctx) {
|
|
|
399
417
|
const safeGetAgent = (sessionId) => {
|
|
400
418
|
try { return ctx.agents?.get?.(sessionId); } catch { return undefined; }
|
|
401
419
|
};
|
|
420
|
+
/** Durable workspace membership for one entity. The record account is the
|
|
421
|
+
* authoritative persisted list on DSH 0.2.x (registry entities expose it as
|
|
422
|
+
* the private `record` field); the runtime `sessionIds` getter is filtered
|
|
423
|
+
* by the canonical-cwd index and may hide members whose index entry is
|
|
424
|
+
* stale, so it is only a fallback for hosts without a readable record. */
|
|
425
|
+
const workspaceMemberIds = (entity) => {
|
|
426
|
+
if (entity === null || entity === undefined) return [];
|
|
427
|
+
const record = entity.record;
|
|
428
|
+
if (record !== null && record !== undefined && Array.isArray(record.sessionIds)) return record.sessionIds;
|
|
429
|
+
const ids = entity.sessionIds;
|
|
430
|
+
if (Array.isArray(ids)) return ids;
|
|
431
|
+
if (typeof ids === "function") {
|
|
432
|
+
try { const value = ids(); if (Array.isArray(value)) return value; } catch { /* ignore */ }
|
|
433
|
+
}
|
|
434
|
+
return [];
|
|
435
|
+
};
|
|
402
436
|
const assertAnnotatable = id => {
|
|
403
437
|
const registry = adapter.workspaceRegistry;
|
|
404
438
|
if (adapter.getSession(id) !== undefined || registry?.headers?.has?.(id)
|
|
405
|
-
|| adapter.workspaceList().some(ws => (ws
|
|
439
|
+
|| adapter.workspaceList().some(ws => workspaceMemberIds(ws).includes(id))) return;
|
|
406
440
|
throw Object.assign(new Error("会话不存在或尚未加载,请先打开该会话后重试"), { code: "session-not-found" });
|
|
407
441
|
};
|
|
408
442
|
const operations = new Map();
|
|
@@ -567,7 +601,9 @@ export function apply(ctx) {
|
|
|
567
601
|
const detachFromWorkspaces = async (sessionId) => {
|
|
568
602
|
const registry = ctx.get("workspaceRegistry") ?? ctx.workspaceRegistry ?? adapter.workspaceRegistry;
|
|
569
603
|
for (const entity of registry.list()) {
|
|
570
|
-
|
|
604
|
+
// Durable record, not the runtime-filtered getter: deleting a session
|
|
605
|
+
// must purge its account even when the cwd index is stale.
|
|
606
|
+
if (workspaceMemberIds(entity).includes(sessionId)) {
|
|
571
607
|
await entity.detachSession(sessionId);
|
|
572
608
|
}
|
|
573
609
|
}
|
|
@@ -576,9 +612,7 @@ export function apply(ctx) {
|
|
|
576
612
|
/** Ordered workspace projection for the client. */
|
|
577
613
|
const listWorkspaces = () => {
|
|
578
614
|
return ctx.workspaceRegistry.list().map((entity) => {
|
|
579
|
-
const rawIds =
|
|
580
|
-
? [...entity.record.sessionIds]
|
|
581
|
-
: [...entity.sessionIds];
|
|
615
|
+
const rawIds = [...workspaceMemberIds(entity)];
|
|
582
616
|
const selfUpdatedAt = entity.updatedAt ?? entity.record?.updatedAt ?? null;
|
|
583
617
|
return {
|
|
584
618
|
id: entity.id,
|
|
@@ -834,6 +868,182 @@ ${rest}`, "utf8");
|
|
|
834
868
|
return false;
|
|
835
869
|
};
|
|
836
870
|
|
|
871
|
+
/**
|
|
872
|
+
* Attach one session to the registered workspace that owns its cwd.
|
|
873
|
+
* Returns true only when a new durable membership was written.
|
|
874
|
+
*
|
|
875
|
+
* DSH's own create/fork flow attaches through workspaceRegistry, but a
|
|
876
|
+
* session created outside that flow (cwd-only create, an attach the host
|
|
877
|
+
* declined, or a session that predates this plugin load) stays
|
|
878
|
+
* unaccounted even though its cwd names a registered workspace. The
|
|
879
|
+
* official entity API re-validates the stored header cwd against the
|
|
880
|
+
* canonical workspace path and persists through the domain table, so a
|
|
881
|
+
* wrong match can never be written. Subagent sessions never join a
|
|
882
|
+
* workspace account, and a session any workspace already accounts for is
|
|
883
|
+
* left untouched (no session is ever registered twice).
|
|
884
|
+
*/
|
|
885
|
+
const attachSessionToOwningWorkspace = async (header) => {
|
|
886
|
+
const registry = adapter.workspaceRegistry;
|
|
887
|
+
if (registry === void 0 || typeof registry.list !== "function") return false;
|
|
888
|
+
if (header === void 0 || typeof header.id !== "string") return false;
|
|
889
|
+
if (header.origin === "subagent") return false;
|
|
890
|
+
if (typeof header.cwd !== "string" || header.cwd === "") return false;
|
|
891
|
+
const run = async () => {
|
|
892
|
+
const entities = registry.list();
|
|
893
|
+
for (const entity of entities) {
|
|
894
|
+
if (workspaceMemberIds(entity).includes(header.id)) return false;
|
|
895
|
+
}
|
|
896
|
+
const byPath = new Map();
|
|
897
|
+
for (const entity of entities) {
|
|
898
|
+
if (typeof entity.path === "string" && !byPath.has(entity.path)) byPath.set(entity.path, entity);
|
|
899
|
+
}
|
|
900
|
+
let target = byPath.get(header.cwd);
|
|
901
|
+
if (target === void 0 && typeof registry.resolveByPath === "function") {
|
|
902
|
+
try { target = await registry.resolveByPath(header.cwd); } catch { target = void 0; }
|
|
903
|
+
}
|
|
904
|
+
if (target === void 0) {
|
|
905
|
+
const canonical = await canonicalizeDirectory(header.cwd);
|
|
906
|
+
if (canonical !== void 0) target = byPath.get(canonical);
|
|
907
|
+
}
|
|
908
|
+
if (target === void 0 || typeof target.attachSession !== "function") return false;
|
|
909
|
+
await target.attachSession(header.id);
|
|
910
|
+
return true;
|
|
911
|
+
};
|
|
912
|
+
if (typeof registry.enqueueOperation === "function") return await registry.enqueueOperation(run);
|
|
913
|
+
return await run();
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Resolve the header the official attachSession() will validate against.
|
|
918
|
+
* WorkspaceEntity.readSessionHeader prefers the live session, then the
|
|
919
|
+
* registry's own header index, and only then a fresh persistence listing.
|
|
920
|
+
* Mirroring that order lets the repair predict a rejection instead of
|
|
921
|
+
* attempting an attach that cannot succeed.
|
|
922
|
+
*/
|
|
923
|
+
const effectiveSessionHeader = (sessionId, storedHeader) => {
|
|
924
|
+
try {
|
|
925
|
+
const live = ctx.sessions?.get?.(sessionId);
|
|
926
|
+
if (live !== undefined && live !== null && live.header !== undefined) return live.header;
|
|
927
|
+
} catch { /* fall through to the index */ }
|
|
928
|
+
try {
|
|
929
|
+
const indexed = adapter.workspaceRegistry?.headers?.get?.(sessionId);
|
|
930
|
+
if (indexed !== undefined) return indexed;
|
|
931
|
+
} catch { /* fall through to the stored header */ }
|
|
932
|
+
return storedHeader;
|
|
933
|
+
};
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Complete the durable workspace membership of a session whose artifact
|
|
937
|
+
* already lives under the target workspace path (same-path move). DSH 0.2.x
|
|
938
|
+
* keeps "cwd matches the workspace path" and "the workspace accounts for
|
|
939
|
+
* the session" as separate facts, so a matching cwd alone must not be
|
|
940
|
+
* reported as success.
|
|
941
|
+
*
|
|
942
|
+
* Conservative policy. The official domain offers no cross-record
|
|
943
|
+
* transaction (single-record updates on one serialized storage unit), and
|
|
944
|
+
* attachSession() validates the session cwd against the workspace path --
|
|
945
|
+
* so an account held by a DIFFERENT workspace can never be restored
|
|
946
|
+
* through the official API once it is removed. Automatic migration would
|
|
947
|
+
* therefore risk destroying the only remaining account if the target
|
|
948
|
+
* attach then failed. Hence:
|
|
949
|
+
* - no workspace accounts for the session: complete the target
|
|
950
|
+
* membership through attachSession(), after pre-checking every
|
|
951
|
+
* condition it validates so a predictable rejection fails before any
|
|
952
|
+
* write;
|
|
953
|
+
* - already accounted by the target and by nobody else: idempotent
|
|
954
|
+
* no-op, no write;
|
|
955
|
+
* - accounted by another workspace: refuse with a membership-conflict
|
|
956
|
+
* error; no automatic cross-record migration;
|
|
957
|
+
* - membership state cannot be confirmed: refuse.
|
|
958
|
+
*
|
|
959
|
+
* No artifact is rewritten, no cwd is touched, and no live session is
|
|
960
|
+
* restarted. A true cross-workspace move re-homes the artifact first and
|
|
961
|
+
* is a separate, unchanged path.
|
|
962
|
+
*
|
|
963
|
+
* @returns {status: "already-member"|"attached", attached: boolean}
|
|
964
|
+
* @throws with code workspace-membership-unknown | workspace-membership-conflict |
|
|
965
|
+
* workspace-attach-unavailable | workspace-repair-unsafe | workspace-repair-failed.
|
|
966
|
+
*/
|
|
967
|
+
const repairWorkspaceMembership = async (target, sessionId, storedHeader) => {
|
|
968
|
+
const registry = ctx.get("workspaceRegistry") ?? ctx.workspaceRegistry ?? adapter.workspaceRegistry;
|
|
969
|
+
const run = async () => {
|
|
970
|
+
// Confirm the current accounting from a fresh, readable listing. An
|
|
971
|
+
// unreadable registry must never be guessed at.
|
|
972
|
+
let entities;
|
|
973
|
+
let inTarget;
|
|
974
|
+
let stale;
|
|
975
|
+
try {
|
|
976
|
+
if (typeof registry?.list !== "function") throw new Error("workspace registry 不可用");
|
|
977
|
+
const listed = registry.list();
|
|
978
|
+
if (!Array.isArray(listed)) throw new Error("workspace registry 返回了无效数据");
|
|
979
|
+
entities = listed;
|
|
980
|
+
inTarget = workspaceMemberIds(target).includes(sessionId);
|
|
981
|
+
stale = entities.filter((entity) =>
|
|
982
|
+
entity !== target && entity.id !== target.id && workspaceMemberIds(entity).includes(sessionId));
|
|
983
|
+
} catch (cause) {
|
|
984
|
+
const error = new Error("无法确认会话当前的工作区归属状态(" + safeErrorMessage(cause) + "),已拒绝修复操作");
|
|
985
|
+
error.code = "workspace-membership-unknown";
|
|
986
|
+
throw error;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
if (stale.length > 0) {
|
|
990
|
+
// Membership conflict: the session is accounted by a workspace whose
|
|
991
|
+
// path differs from its cwd. That account cannot be restored through
|
|
992
|
+
// the official API once removed, so an automatic migration could
|
|
993
|
+
// destroy the only remaining account if the target attach failed.
|
|
994
|
+
// Refuse and let the operator resolve the conflict.
|
|
995
|
+
const names = stale.map((entity) => entity.title || entity.id).join("、");
|
|
996
|
+
const error = new Error(
|
|
997
|
+
"会话当前登记在其他工作区(" + names + "),与其 cwd 指向的目标工作区不一致。"
|
|
998
|
+
+ "为避免修复失败造成数据损失,已拒绝自动迁移;请先处理该归属冲突,或使用真正的跨工作区移动"
|
|
999
|
+
);
|
|
1000
|
+
error.code = "workspace-membership-conflict";
|
|
1001
|
+
throw error;
|
|
1002
|
+
}
|
|
1003
|
+
if (inTarget) return { status: "already-member", attached: false };
|
|
1004
|
+
if (typeof target.attachSession !== "function") {
|
|
1005
|
+
const error = new Error("当前 DSH 宿主未提供官方工作区归属接口(attachSession),无法补全会话归属");
|
|
1006
|
+
error.code = "workspace-attach-unavailable";
|
|
1007
|
+
throw error;
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// Pre-check every condition attachSession() validates so a predictable
|
|
1011
|
+
// rejection fails before any write. The header it reads is the live
|
|
1012
|
+
// session first, then the registry index, then a fresh persistence
|
|
1013
|
+
// listing -- not necessarily the header this request was validated
|
|
1014
|
+
// against.
|
|
1015
|
+
const effective = effectiveSessionHeader(sessionId, storedHeader);
|
|
1016
|
+
if (effective === void 0 || typeof effective.cwd !== "string" || effective.cwd === "") {
|
|
1017
|
+
const error = new Error("拒绝补全归属:会话没有可校验的 cwd,官方接口无法登记");
|
|
1018
|
+
error.code = "workspace-repair-unsafe";
|
|
1019
|
+
throw error;
|
|
1020
|
+
}
|
|
1021
|
+
const canonical = await canonicalizeDirectory(effective.cwd);
|
|
1022
|
+
if (canonical === void 0 || canonical !== target.path) {
|
|
1023
|
+
const error = new Error(
|
|
1024
|
+
"拒绝补全归属:会话当前 cwd(" + effective.cwd + ")解析后与目标工作区(" + target.path
|
|
1025
|
+
+ ")不一致,官方接口会拒绝登记;未做任何修改"
|
|
1026
|
+
);
|
|
1027
|
+
error.code = "workspace-repair-unsafe";
|
|
1028
|
+
throw error;
|
|
1029
|
+
}
|
|
1030
|
+
try {
|
|
1031
|
+
await target.attachSession(sessionId);
|
|
1032
|
+
} catch (cause) {
|
|
1033
|
+
// Nothing has been written yet, so the previous state (no account) is
|
|
1034
|
+
// intact; surface the failure with its cause.
|
|
1035
|
+
const error = new Error("补全归属失败:" + safeErrorMessage(cause));
|
|
1036
|
+
error.code = "workspace-repair-failed";
|
|
1037
|
+
throw error;
|
|
1038
|
+
}
|
|
1039
|
+
return { status: "attached", attached: true };
|
|
1040
|
+
};
|
|
1041
|
+
if (registry !== void 0 && typeof registry.enqueueOperation === "function") {
|
|
1042
|
+
return await registry.enqueueOperation(run);
|
|
1043
|
+
}
|
|
1044
|
+
return await run();
|
|
1045
|
+
};
|
|
1046
|
+
|
|
837
1047
|
/**
|
|
838
1048
|
* TRUE cross-workspace move. See the module docstring for the full design:
|
|
839
1049
|
* re-home the stored cwd, migrate the artifact, then swap accounting.
|
|
@@ -896,7 +1106,25 @@ ${rest}`, "utf8");
|
|
|
896
1106
|
let currentCanonical;
|
|
897
1107
|
try { currentCanonical = await realpath(storedHeader.cwd); } catch { /* re-home below */ }
|
|
898
1108
|
if (currentCanonical === targetPath) {
|
|
899
|
-
|
|
1109
|
+
// The artifact already lives under the target workspace path, but on
|
|
1110
|
+
// DSH 0.2.x "cwd matches" and "the workspace accounts for the
|
|
1111
|
+
// session" are separate facts: a matching cwd with a missing
|
|
1112
|
+
// registration used to return a silent no-op success. Repair the
|
|
1113
|
+
// durable membership through the official entity API (idempotent,
|
|
1114
|
+
// no artifact rewrite, no live-session restart) and report what
|
|
1115
|
+
// actually happened. A repair that cannot be completed fails loudly
|
|
1116
|
+
// instead of reporting success.
|
|
1117
|
+
const repair = await repairWorkspaceMembership(target, sessionId, storedHeader);
|
|
1118
|
+
return {
|
|
1119
|
+
ok: true,
|
|
1120
|
+
sessionId,
|
|
1121
|
+
moved: false,
|
|
1122
|
+
attached: repair.attached,
|
|
1123
|
+
membership: repair.status,
|
|
1124
|
+
message: repair.attached
|
|
1125
|
+
? "会话已归属目标工作区(已补全归属记录)"
|
|
1126
|
+
: "会话已属于目标工作区"
|
|
1127
|
+
};
|
|
900
1128
|
}
|
|
901
1129
|
}
|
|
902
1130
|
|
|
@@ -1003,7 +1231,7 @@ ${rest}`, "utf8");
|
|
|
1003
1231
|
const oldIndexedPath = registry.sessionPaths?.get?.(sessionId);
|
|
1004
1232
|
const oldInvalidPath = registry.invalidSessionPaths?.get?.(sessionId);
|
|
1005
1233
|
const fromWorkspaceIds = registry.list()
|
|
1006
|
-
.filter((entity) => entity.id !== target.id && (entity
|
|
1234
|
+
.filter((entity) => entity.id !== target.id && workspaceMemberIds(entity).includes(sessionId))
|
|
1007
1235
|
.map((entity) => entity.id);
|
|
1008
1236
|
let targetAttached = false;
|
|
1009
1237
|
const detachedFrom = [];
|
|
@@ -1168,8 +1396,7 @@ ${rest}`, "utf8");
|
|
|
1168
1396
|
throw error;
|
|
1169
1397
|
}
|
|
1170
1398
|
allowedIds = new Set();
|
|
1171
|
-
const
|
|
1172
|
-
if (Array.isArray(ids)) for (const id of ids) allowedIds.add(id);
|
|
1399
|
+
for (const id of workspaceMemberIds(ws)) allowedIds.add(id);
|
|
1173
1400
|
}
|
|
1174
1401
|
}
|
|
1175
1402
|
const requestedSessionId = filter !== void 0 && typeof filter === "object" && typeof filter.sessionId === "string"
|
|
@@ -1818,43 +2045,103 @@ ${rest}`, "utf8");
|
|
|
1818
2045
|
if (typeof persistence?.list !== "function" || typeof registry?.list !== "function") return;
|
|
1819
2046
|
|
|
1820
2047
|
try {
|
|
1821
|
-
//
|
|
1822
|
-
//
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
2048
|
+
// Snapshot durable membership BEFORE the async scan. Only ids that
|
|
2049
|
+
// already existed here may ever be pruned later; ids attached
|
|
2050
|
+
// concurrently must never be removed using an older disk snapshot.
|
|
2051
|
+
let entities;
|
|
2052
|
+
try { entities = registry.list(); }
|
|
2053
|
+
catch (error) {
|
|
2054
|
+
ctx.logger.warn("session-manager: workspace registry unavailable; workspace reconciliation skipped: " + safeErrorMessage(error));
|
|
1827
2055
|
return;
|
|
1828
2056
|
}
|
|
1829
|
-
const
|
|
1830
|
-
const
|
|
2057
|
+
const initialIds = new Map(entities.map(ws => [ws.id, new Set(workspaceMemberIds(ws))]));
|
|
2058
|
+
const scan = await scanSessionHeaders(persistence, adapter.homePath);
|
|
2059
|
+
const headers = Array.isArray(scan.headers) ? scan.headers : [];
|
|
2060
|
+
|
|
2061
|
+
// Step 1a: attach sessions whose cwd names a registered workspace but
|
|
2062
|
+
// that no workspace accounts for (issue #41). Every candidate is
|
|
2063
|
+
// re-validated by the official entity API, which persists through the
|
|
2064
|
+
// domain table and is idempotent for already-accounted ids -- so this
|
|
2065
|
+
// pass can only add well-formed memberships, never corrupt records.
|
|
2066
|
+
// It runs whenever the persistence index listed successfully
|
|
2067
|
+
// (scan.persisted); a partial disk sweep cannot make a listed header
|
|
2068
|
+
// invalid.
|
|
2069
|
+
let added = 0;
|
|
2070
|
+
if (scan.persisted) {
|
|
2071
|
+
const assigned = new Set();
|
|
2072
|
+
for (const ws of entities) for (const id of initialIds.get(ws.id)) assigned.add(id);
|
|
2073
|
+
const candidates = headers.filter(header =>
|
|
2074
|
+
typeof header.cwd === "string" && header.cwd !== ""
|
|
2075
|
+
&& header.origin !== "subagent"
|
|
2076
|
+
&& !assigned.has(header.id));
|
|
2077
|
+
if (candidates.length > 0) {
|
|
2078
|
+
const byPath = new Map();
|
|
2079
|
+
for (const ws of entities) {
|
|
2080
|
+
if (typeof ws.path === "string" && !byPath.has(ws.path)) byPath.set(ws.path, ws);
|
|
2081
|
+
}
|
|
2082
|
+
const attach = async () => {
|
|
2083
|
+
// Re-read membership inside the registry write queue: a concurrent
|
|
2084
|
+
// move may have accounted an id after the snapshot above.
|
|
2085
|
+
const current = typeof registry.list === "function" ? registry.list() : entities;
|
|
2086
|
+
const assignedNow = new Set();
|
|
2087
|
+
for (const ws of current) for (const id of workspaceMemberIds(ws)) assignedNow.add(id);
|
|
2088
|
+
for (const header of candidates) {
|
|
2089
|
+
if (assignedNow.has(header.id)) continue;
|
|
2090
|
+
let target = byPath.get(header.cwd);
|
|
2091
|
+
if (target === void 0) {
|
|
2092
|
+
const canonical = await canonicalizeDirectory(header.cwd);
|
|
2093
|
+
if (canonical !== void 0) target = byPath.get(canonical);
|
|
2094
|
+
}
|
|
2095
|
+
if (target === void 0 || typeof target.attachSession !== "function") continue;
|
|
2096
|
+
try {
|
|
2097
|
+
await target.attachSession(header.id);
|
|
2098
|
+
assignedNow.add(header.id);
|
|
2099
|
+
added += 1;
|
|
2100
|
+
} catch (error) {
|
|
2101
|
+
ctx.logger.warn("session-manager: boot attach of session '" + header.id + "' to workspace '" + target.id + "' failed: " + safeErrorMessage(error));
|
|
2102
|
+
}
|
|
2103
|
+
}
|
|
2104
|
+
};
|
|
2105
|
+
if (typeof registry.enqueueOperation === "function") await registry.enqueueOperation(attach);
|
|
2106
|
+
else await attach();
|
|
2107
|
+
}
|
|
2108
|
+
}
|
|
1831
2109
|
|
|
1832
|
-
// Step
|
|
2110
|
+
// Step 1b: prune record entries whose session no longer exists. Only a
|
|
2111
|
+
// fully complete scan (persistence index plus disk sweep, no errors)
|
|
2112
|
+
// may remove anything; live sessions and ids attached after the
|
|
2113
|
+
// snapshot are always kept. Durable records are read through
|
|
2114
|
+
// workspaceMemberIds so a stale runtime index cannot hide a dead entry.
|
|
1833
2115
|
let removed = 0;
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
const
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
assignedIds.add(header.id);
|
|
1849
|
-
added += 1;
|
|
2116
|
+
if (scan.complete) {
|
|
2117
|
+
const validIds = new Set(headers.map(header => header.id));
|
|
2118
|
+
const prune = async () => {
|
|
2119
|
+
for (const ws of (typeof registry.list === "function" ? registry.list() : entities)) {
|
|
2120
|
+
const initial = initialIds.get(ws.id);
|
|
2121
|
+
// Snapshot: detachSession replaces the record array while we walk it.
|
|
2122
|
+
for (const id of [...workspaceMemberIds(ws)]) {
|
|
2123
|
+
if (validIds.has(id)) continue;
|
|
2124
|
+
if (initial === void 0 || !initial.has(id)) continue;
|
|
2125
|
+
if (adapter.getSession(id) !== undefined) continue;
|
|
2126
|
+
if (typeof ws.detachSession !== "function") continue;
|
|
2127
|
+
await ws.detachSession(id);
|
|
2128
|
+
removed += 1;
|
|
2129
|
+
}
|
|
1850
2130
|
}
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
2131
|
+
};
|
|
2132
|
+
if (typeof registry.enqueueOperation === "function") await registry.enqueueOperation(prune);
|
|
2133
|
+
else await prune();
|
|
2134
|
+
}
|
|
2135
|
+
if (added || removed) {
|
|
2136
|
+
ctx.logger.info('session-manager: post-boot workspace reconcile attached ' + added + ' unaccounted sessions, removed ' + removed + ' orphans');
|
|
2137
|
+
}
|
|
2138
|
+
if (!scan.persisted) {
|
|
2139
|
+
ctx.logger.warn("session-manager: incomplete session scan; workspace reconciliation skipped: " + scan.errors.map(safeErrorMessage).join("; "));
|
|
2140
|
+
return;
|
|
2141
|
+
}
|
|
2142
|
+
if (!scan.complete) {
|
|
2143
|
+
ctx.logger.warn("session-manager: incomplete disk sweep; workspace membership pruning skipped: " + scan.errors.map(safeErrorMessage).join("; "));
|
|
2144
|
+
return;
|
|
1858
2145
|
}
|
|
1859
2146
|
|
|
1860
2147
|
// Step 2: Budgeted Background Projection Cache Rebuild
|
|
@@ -1918,6 +2205,32 @@ ${rest}`, "utf8");
|
|
|
1918
2205
|
}
|
|
1919
2206
|
}, "session-manager: post-boot re-fold stale projection cache rows");
|
|
1920
2207
|
|
|
2208
|
+
/**
|
|
2209
|
+
* Runtime membership repair (issue #41): DSH's own create/fork flow attaches
|
|
2210
|
+
* a session to its workspace, but a session created outside that flow -- or
|
|
2211
|
+
* one whose host-side attach failed -- stays unaccounted even though its
|
|
2212
|
+
* cwd names a registered workspace, and would otherwise only be repaired on
|
|
2213
|
+
* the next DSH restart. Repair it as soon as the session is announced.
|
|
2214
|
+
*
|
|
2215
|
+
* The listener must never throw synchronously: a throwing session/created
|
|
2216
|
+
* listener rolls back session creation in DSH. Everything is contained and
|
|
2217
|
+
* fire-and-forget; failures are logged only.
|
|
2218
|
+
*/
|
|
2219
|
+
if (typeof ctx.on === "function") ctx.on("session/created", (session) => {
|
|
2220
|
+
void (async () => {
|
|
2221
|
+
try {
|
|
2222
|
+
const header = session?.header ?? session;
|
|
2223
|
+
if (header === void 0 || typeof header.id !== "string") return;
|
|
2224
|
+
const attached = await attachSessionToOwningWorkspace(header);
|
|
2225
|
+
if (attached) {
|
|
2226
|
+
ctx.logger.info("session-manager: attached new session '" + header.id + "' to its cwd workspace");
|
|
2227
|
+
}
|
|
2228
|
+
} catch (error) {
|
|
2229
|
+
ctx.logger.warn("session-manager: session/created membership repair failed: " + safeErrorMessage(error));
|
|
2230
|
+
}
|
|
2231
|
+
})();
|
|
2232
|
+
});
|
|
2233
|
+
|
|
1921
2234
|
/** Build a normalized annotation patch from one bulk action + payload.
|
|
1922
2235
|
* Returns null when the payload is missing or malformed so the caller can
|
|
1923
2236
|
* surface a 400 instead of letting the store throw on a bad value.
|
|
@@ -2215,7 +2528,7 @@ ${rest}`, "utf8");
|
|
|
2215
2528
|
return send(res, 200, { ok: true, result });
|
|
2216
2529
|
}
|
|
2217
2530
|
if (req.method === "GET" && path === "/update/check") {
|
|
2218
|
-
const currentVersion = adapter.pluginVersion() || "0.7.
|
|
2531
|
+
const currentVersion = adapter.pluginVersion() || "0.7.4";
|
|
2219
2532
|
const runningVersion = currentVersion;
|
|
2220
2533
|
let installedVersion = runningVersion;
|
|
2221
2534
|
const profileDir = resolveProfileDir(adapter, ctx);
|
|
@@ -2399,7 +2712,7 @@ ${rest}`, "utf8");
|
|
|
2399
2712
|
};
|
|
2400
2713
|
setActiveTask(taskRecord);
|
|
2401
2714
|
|
|
2402
|
-
const currentVersion = adapter.pluginVersion() || "0.7.
|
|
2715
|
+
const currentVersion = adapter.pluginVersion() || "0.7.4";
|
|
2403
2716
|
let installedVersion = currentVersion;
|
|
2404
2717
|
if (profileDir) {
|
|
2405
2718
|
const pkgFile = join(profileDir, "node_modules", "dsh-session-manager", "package.json");
|
|
@@ -2929,7 +3242,7 @@ ${rest}`, "utf8");
|
|
|
2929
3242
|
}
|
|
2930
3243
|
if (req.method === "GET" && path === "/update/status") {
|
|
2931
3244
|
const profileDir = resolveProfileDir(adapter, ctx);
|
|
2932
|
-
const currentVersion = adapter.pluginVersion() || "0.7.
|
|
3245
|
+
const currentVersion = adapter.pluginVersion() || "0.7.4";
|
|
2933
3246
|
let installedVersion = currentVersion;
|
|
2934
3247
|
if (profileDir) {
|
|
2935
3248
|
const diskCheck = verifyInstalledPackage(profileDir);
|
|
@@ -3004,7 +3317,9 @@ ${rest}`, "utf8");
|
|
|
3004
3317
|
ctx.logger.warn("session-manager: api error: " + safeErrorMessage(error) + "; json=" + safeJson(error));
|
|
3005
3318
|
const status = error?.code === "bad-request" ? 400 : error?.code === "body-too-large" ? 413
|
|
3006
3319
|
: error?.code === "annotation-conflict" || error?.code === "annotations-busy" ? 409
|
|
3007
|
-
: error?.code === "session-not-found" ? 404 : error?.code?.startsWith("annotations-") ? 503
|
|
3320
|
+
: error?.code === "session-not-found" ? 404 : error?.code?.startsWith("annotations-") ? 503
|
|
3321
|
+
: error?.code === "workspace-membership-conflict" ? 409
|
|
3322
|
+
: error?.code === "workspace-attach-unavailable" ? 501 : 500;
|
|
3008
3323
|
return send(res, status, {
|
|
3009
3324
|
ok: false,
|
|
3010
3325
|
error: safeErrorMessage(error),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-session-manager",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "DeepSeek Harness session and workspace manager —— Workspaces: archive and visibility. Sessions: move, migrate presets, delete, favorite, review later, prioritize, tag, and annotate. Batch operations. | DSH 会话与工作区管理——工作区:归档与显示。会话:跨工作区移动、预设迁移、删除;收藏、待回看、优先级、标签、备注。批量操作。",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|