@michengai/dsh-archive-manager 0.1.31 → 0.1.32
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 +6 -0
- package/CHANGELOG.zh-CN.md +6 -0
- package/README.md +4 -1
- package/README.zh-CN.md +4 -1
- package/lib/workspace.js +41 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,12 @@ Published release notes are retained below; new versions are added without remov
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.1.32 - 2026-09-07
|
|
10
|
+
|
|
11
|
+
- Fixed leftover session directories after deletion (#16): remove the entire session-owned directory after validating the official JSONL layout, preserve shared/unknown-backend parent directories, and refuse deletion through directory links.
|
|
12
|
+
- Added regression coverage for directory and attachment cleanup, path encoding, shared-directory protection, and failed-deletion retries.
|
|
13
|
+
- Kept relative JSONL storage roots stable across host working-directory changes and added warnings when directory ownership cannot be verified.
|
|
14
|
+
|
|
9
15
|
## 0.1.31 - 2026-09-07
|
|
10
16
|
|
|
11
17
|
- Added independent in-product update checks with automatic updates when a verified DSH update service is available and a profile-specific manual fallback otherwise.
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -6,6 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
## 未发布
|
|
8
8
|
|
|
9
|
+
## 0.1.32 - 2026-09-07
|
|
10
|
+
|
|
11
|
+
- 修复删除会话后遗留目录的问题(#16):校验官方 JSONL 存储布局后清理完整会话专属目录,保留共享目录和未知后端的父目录,并拒绝通过目录符号链接进行删除。
|
|
12
|
+
- 补充目录与附件清理、路径编码、共享目录保护及删除失败重试的回归测试。
|
|
13
|
+
- 修复宿主工作目录变化导致相对 JSONL 存储根路径失效的问题,并为无法验证目录归属的降级删除增加警告。
|
|
14
|
+
|
|
9
15
|
## 0.1.31 - 2026-09-07
|
|
10
16
|
|
|
11
17
|
- 新增独立的插件内检查更新能力:仅在已验证的 DSH 更新服务可用时自动更新,其余环境提供与 Profile 对应的手工更新命令。
|
package/README.md
CHANGED
|
@@ -146,7 +146,10 @@ If the entry is missing after installation or upgrade, restart DSH Web and hard-
|
|
|
146
146
|
## Data handling limits
|
|
147
147
|
|
|
148
148
|
- Deletion always requires confirmation.
|
|
149
|
-
- It removes
|
|
149
|
+
- It removes workspace records, archive markers, and the projection cache. For the official JSONL backend, a validated layout also allows removal of the session-owned directory and its contents, including attachments. Other backends or unknown layouts only lose the located transcript artifact, never its parent directory.
|
|
150
|
+
- Project containers and storage roots are retained. Deletion refuses symbolic links or Windows junctions at the official layout's project/session directory levels and keeps the operation retryable.
|
|
151
|
+
- Layout validation uses the official backend's initialized absolute root, so a relative root remains stable across host working-directory changes. If that field is unavailable, only an absolute configured root is accepted. An unverified official JSONL layout emits a warning with the session ID and artifact path before falling back to artifact-only deletion.
|
|
152
|
+
- Directory validation is not a cross-process filesystem lock: do not concurrently move or replace storage directories or change directory links during deletion. Storage paths writable by untrusted processes are not a security isolation boundary.
|
|
150
153
|
- A live session finishes writing before cleanup to prevent data truncation.
|
|
151
154
|
- The plugin replaces DSH’s default workspace and projection services. Install through the DSH profile instead of manually composing the patch.
|
|
152
155
|
|
package/README.zh-CN.md
CHANGED
|
@@ -146,7 +146,10 @@ dsh --profile web --dump-config
|
|
|
146
146
|
## 数据处理边界
|
|
147
147
|
|
|
148
148
|
- 删除操作始终需要确认。
|
|
149
|
-
-
|
|
149
|
+
- 删除会移除工作区记录、归档标记和投影缓存。官方 JSONL 后端在目录布局校验通过后,会一并删除会话专属目录及其中的附件等内容;其他后端或未知布局仅删除定位到的转录工件,不删除其父目录。
|
|
150
|
+
- 不清理项目分组目录或存储根目录。官方布局的项目/会话目录若为符号链接或 Windows junction,会拒绝删除并保留可重试状态。
|
|
151
|
+
- 布局校验优先使用官方后端初始化时确定的绝对根路径,相对路径配置不再受宿主工作目录变化影响;若无法取得该字段,则只接受绝对路径配置。官方 JSONL 布局校验失败时,会记录包含会话 ID 和工件路径的警告,再降级为仅删除工件。
|
|
152
|
+
- 目录校验不是跨进程文件锁:删除期间不要由其他进程迁移、替换存储目录或改写目录链接。插件不将可被不可信进程改写的存储路径视为安全隔离边界。
|
|
150
153
|
- 正在写入的会话会在完成写入后清理,避免截断数据。
|
|
151
154
|
- 本插件替换 DSH 默认的工作区和会话投影服务;请仅通过 DSH profile 安装,避免手工拼接补丁配置。
|
|
152
155
|
|
package/lib/workspace.js
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
|
-
import { rm } from "node:fs/promises";
|
|
1
|
+
import { lstat, rm } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
2
3
|
import {
|
|
3
4
|
WorkspaceRegistry
|
|
4
5
|
} from "@deepseek-ai/dsh-workspace";
|
|
5
6
|
import { bindTypertRemote, Remote } from "@deepseek-ai/dsh-typert-protocol";
|
|
6
7
|
import { sessionDir } from "@deepseek-ai/dsh-spill-local";
|
|
7
8
|
import { trackTombstone } from "./tombstone.js";
|
|
9
|
+
function jsonlSessionDirectory(persistence, header, location) {
|
|
10
|
+
if (persistence.name !== "session-persistence-jsonl" || location.kind !== "jsonl") return;
|
|
11
|
+
const root = persistence.root ?? persistence.config?.root;
|
|
12
|
+
if (typeof root !== "string" || !isAbsolute(root) || !isAbsolute(location.path)) return;
|
|
13
|
+
if (!["session.jsonl", "session.jsonl.zstd"].includes(basename(location.path))) return;
|
|
14
|
+
if (typeof header.id !== "string" || header.id.length === 0) return;
|
|
15
|
+
const encode = (text) => text.replace(/[^A-Za-z0-9._-]/g, (ch) => `~${ch.charCodeAt(0).toString(16).toUpperCase().padStart(4, "0")}`);
|
|
16
|
+
const segment = header.id === "." ? "~002E" : header.id === ".." ? "~002E~002E" : encode(header.id);
|
|
17
|
+
let project = "_no-cwd";
|
|
18
|
+
if (header.cwd !== void 0) {
|
|
19
|
+
if (typeof header.cwd !== "string" || header.cwd.length === 0) return;
|
|
20
|
+
const readable = encode(header.cwd.replace(/[\\/:]+/g, "-")).replace(/^-+/, "") || "root";
|
|
21
|
+
project = `--${readable.slice(0, 251)}--`;
|
|
22
|
+
}
|
|
23
|
+
const directory = join(resolve(root), project, segment);
|
|
24
|
+
if (location.path !== join(directory, basename(location.path))) return;
|
|
25
|
+
return directory;
|
|
26
|
+
}
|
|
8
27
|
function markRemoteMethod(instance, method) {
|
|
9
28
|
const context = {
|
|
10
29
|
private: false,
|
|
@@ -577,7 +596,7 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
577
596
|
this.ctx.logger.warn(`archive-manager: could not publish removal for stored session "${sessionId}": ${String(error)}`);
|
|
578
597
|
}
|
|
579
598
|
}
|
|
580
|
-
/**
|
|
599
|
+
/** 官方 JSONL 已知布局清理会话专属目录;其他后端只删除定位到的工件。 */
|
|
581
600
|
async removeTranscriptDirectory(sessionId) {
|
|
582
601
|
const persistence = this.ctx.get("sessionPersistence");
|
|
583
602
|
if (persistence === void 0 || typeof persistence.locate !== "function") {
|
|
@@ -588,10 +607,28 @@ var ArchiveWorkspaceRegistry = class extends WorkspaceRegistry {
|
|
|
588
607
|
if (location === void 0 || typeof location.path !== "string") {
|
|
589
608
|
throw new Error(`cannot delete session "${sessionId}": the session persistence backend could not resolve its transcript artifact`);
|
|
590
609
|
}
|
|
610
|
+
let target = { path: location.path, kind: "transcript artifact" };
|
|
591
611
|
try {
|
|
592
|
-
|
|
612
|
+
const directory = jsonlSessionDirectory(persistence, header, location);
|
|
613
|
+
if (directory !== void 0) {
|
|
614
|
+
target = { path: directory, kind: "session directory" };
|
|
615
|
+
for (const path of [dirname(directory), directory]) {
|
|
616
|
+
let stat;
|
|
617
|
+
try {
|
|
618
|
+
stat = await lstat(path);
|
|
619
|
+
} catch (error) {
|
|
620
|
+
if (error?.code === "ENOENT") continue;
|
|
621
|
+
throw error;
|
|
622
|
+
}
|
|
623
|
+
if (stat.isSymbolicLink()) throw new Error(`refusing to delete through symbolic link "${path}"`);
|
|
624
|
+
if (!stat.isDirectory()) throw new Error(`expected session storage directory "${path}"`);
|
|
625
|
+
}
|
|
626
|
+
} else if (persistence.name === "session-persistence-jsonl") {
|
|
627
|
+
this.ctx.logger.warn(`archive-manager: session "${sessionId}": JSONL directory ownership could not be verified; falling back to artifact-only deletion at "${location.path}" (parent directory retained)`);
|
|
628
|
+
}
|
|
629
|
+
await rm(target.path, { recursive: true, force: true });
|
|
593
630
|
} catch (error) {
|
|
594
|
-
const message = `cannot delete session "${sessionId}":
|
|
631
|
+
const message = `cannot delete session "${sessionId}": cleanup of ${target.kind} "${target.path}" failed; bookkeeping retained for retry`;
|
|
595
632
|
const detail = `${message}: ${String(error)}`;
|
|
596
633
|
this.ctx.logger.warn(`archive-manager: ${detail}`);
|
|
597
634
|
throw new Error(detail, { cause: error });
|