dsh-conversation-recycle-bin 0.3.11 → 0.3.13
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 +454 -412
- package/README.md +8 -8
- package/client.js +75 -38
- package/lib/core/cascade.js +56 -23
- package/lib/core/file-lock.js +43 -8
- package/lib/core/policy.js +126 -119
- package/lib/core/trash.js +245 -227
- package/lib/host/api.js +182 -37
- package/lib/host/catalog.js +9 -2
- package/lib/host/gateway.js +368 -351
- package/lib/host/paths.js +136 -1
- package/lib/version.js +8 -8
- package/package.json +81 -81
package/lib/host/paths.js
CHANGED
|
@@ -9,8 +9,12 @@
|
|
|
9
9
|
* Session directories are discovered by scanning `<DSH_HOME>/sessions/<slug>/`
|
|
10
10
|
* instead of re-implementing the slug encoding: the slug is a Host implementation
|
|
11
11
|
* detail, while the session id directory name is part of the durable identity.
|
|
12
|
+
* The same encoding is reconstructed only by crash recovery (`recoverSessionLog`
|
|
13
|
+
* in `host/api.js`), where the moved log header is the sole remaining source of
|
|
14
|
+
* its original cwd and there is no directory left to scan.
|
|
12
15
|
*/
|
|
13
|
-
import {
|
|
16
|
+
import { createReadStream } from 'node:fs'
|
|
17
|
+
import { cp, mkdir, open, readdir, readFile, rename, rm, stat } from 'node:fs/promises'
|
|
14
18
|
import { homedir } from 'node:os'
|
|
15
19
|
import { dirname, join, resolve } from 'node:path'
|
|
16
20
|
|
|
@@ -62,6 +66,137 @@ export function decodeSessionSegment(name) {
|
|
|
62
66
|
return String(name).replace(/~([0-9A-Fa-f]{4})/g, (_, hex) => String.fromCharCode(Number.parseInt(hex, 16)))
|
|
63
67
|
}
|
|
64
68
|
|
|
69
|
+
/**
|
|
70
|
+
* The Host's project-directory key for a session cwd.
|
|
71
|
+
*
|
|
72
|
+
* Recovery needs the exact directory a moved log came from, and the log itself
|
|
73
|
+
* is the only place its cwd still exists. This mirrors
|
|
74
|
+
* `@deepseek-ai/dsh-session-persistence-jsonl`'s `projectKey` (separators and
|
|
75
|
+
* drive colons become `-`, unsafe code units escape as `~XXXX`, bounded to
|
|
76
|
+
* 251 chars) so a reconstructed path is byte-identical to the original one.
|
|
77
|
+
*/
|
|
78
|
+
export function projectKey(cwd) {
|
|
79
|
+
if (typeof cwd !== 'string' || cwd.length === 0) throw new Error('cannot encode an empty project path')
|
|
80
|
+
let readable = ''
|
|
81
|
+
let separatorRun = false
|
|
82
|
+
for (let i = 0; i < cwd.length; i += 1) {
|
|
83
|
+
const code = cwd.charCodeAt(i)
|
|
84
|
+
const ch = String.fromCharCode(code)
|
|
85
|
+
if (ch === '/' || ch === '\\' || ch === ':') {
|
|
86
|
+
if (!separatorRun) readable += '-'
|
|
87
|
+
separatorRun = true
|
|
88
|
+
} else if (ch !== '~' && /^[A-Za-z0-9._-]$/.test(ch)) {
|
|
89
|
+
readable += ch
|
|
90
|
+
separatorRun = false
|
|
91
|
+
} else {
|
|
92
|
+
readable += `~${code.toString(16).toUpperCase().padStart(4, '0')}`
|
|
93
|
+
separatorRun = false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return `--${(readable.replace(/^-+/, '') || 'root').slice(0, 251)}--`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The Host's path encoding for one session id; ids normally need no escaping. */
|
|
100
|
+
export function encodeSessionSegment(sessionId) {
|
|
101
|
+
const raw = String(sessionId ?? '')
|
|
102
|
+
if (raw.length === 0) throw new Error('cannot encode an empty session id')
|
|
103
|
+
if (raw === '.') return '~002E'
|
|
104
|
+
if (raw === '..') return '~002E~002E'
|
|
105
|
+
let out = ''
|
|
106
|
+
for (let i = 0; i < raw.length; i += 1) {
|
|
107
|
+
const code = raw.charCodeAt(i)
|
|
108
|
+
const ch = String.fromCharCode(code)
|
|
109
|
+
if (ch !== '~' && /^[A-Za-z0-9._-]$/.test(ch)) out += ch
|
|
110
|
+
else out += `~${code.toString(16).toUpperCase().padStart(4, '0')}`
|
|
111
|
+
}
|
|
112
|
+
return out
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const SESSION_HEADER_BYTE_LIMIT = 256 * 1024
|
|
116
|
+
|
|
117
|
+
/** Read the first line of a plain `.jsonl` log without loading the whole file. */
|
|
118
|
+
async function readFirstLine(file) {
|
|
119
|
+
let handle
|
|
120
|
+
try {
|
|
121
|
+
handle = await open(file, 'r')
|
|
122
|
+
const buffer = Buffer.alloc(64 * 1024)
|
|
123
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0)
|
|
124
|
+
const text = buffer.subarray(0, bytesRead).toString('utf8')
|
|
125
|
+
const newline = text.indexOf('\n')
|
|
126
|
+
return newline === -1 ? text : text.slice(0, newline)
|
|
127
|
+
} finally {
|
|
128
|
+
await handle?.close().catch(() => {})
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Read the first line of a zstd-compressed log, stopping after the header.
|
|
134
|
+
*
|
|
135
|
+
* `node:zlib` only gained zstd in Node 22.15; this package declares Node >=20,
|
|
136
|
+
* so the codec is loaded lazily and an older runtime simply fails recovery
|
|
137
|
+
* (the caller keeps refusing) instead of failing to import the plugin.
|
|
138
|
+
*/
|
|
139
|
+
async function readZstdFirstLine(file) {
|
|
140
|
+
const zlib = await import('node:zlib')
|
|
141
|
+
const createDecoder = zlib.createZstdDecompress
|
|
142
|
+
if (typeof createDecoder !== 'function') throw new Error('zstd decompression is unavailable on this Node runtime')
|
|
143
|
+
return new Promise((resolve, reject) => {
|
|
144
|
+
const decoder = createDecoder()
|
|
145
|
+
const input = createReadStream(file)
|
|
146
|
+
let text = ''
|
|
147
|
+
let settled = false
|
|
148
|
+
const finish = (error, value) => {
|
|
149
|
+
if (settled) return
|
|
150
|
+
settled = true
|
|
151
|
+
input.destroy()
|
|
152
|
+
decoder.destroy()
|
|
153
|
+
if (error !== undefined) reject(error)
|
|
154
|
+
else resolve(value)
|
|
155
|
+
}
|
|
156
|
+
input.on('error', (error) => finish(error))
|
|
157
|
+
decoder.on('error', (error) => finish(error))
|
|
158
|
+
decoder.on('data', (chunk) => {
|
|
159
|
+
text += chunk.toString('utf8')
|
|
160
|
+
const newline = text.indexOf('\n')
|
|
161
|
+
if (newline !== -1) finish(undefined, text.slice(0, newline))
|
|
162
|
+
else if (text.length > SESSION_HEADER_BYTE_LIMIT) finish(new Error('session header is too large'))
|
|
163
|
+
})
|
|
164
|
+
decoder.on('end', () => finish(undefined, text.split('\n')[0]))
|
|
165
|
+
input.pipe(decoder)
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* The `cwd` recorded in a session log's first header line, or null when the
|
|
171
|
+
* directory has no readable log. Used only to reconstruct the original path of
|
|
172
|
+
* a log a crashed hard delete already moved into the recycle bin.
|
|
173
|
+
*/
|
|
174
|
+
export async function readSessionLogCwd(sessionDir) {
|
|
175
|
+
let names = []
|
|
176
|
+
try {
|
|
177
|
+
names = (await readdir(sessionDir)).filter((name) => /\.jsonl(?:\.zstd)?$/u.test(name))
|
|
178
|
+
} catch {
|
|
179
|
+
return null
|
|
180
|
+
}
|
|
181
|
+
const ranked = names.sort((left, right) => {
|
|
182
|
+
const rank = (name) => (name.includes('v4') ? 0 : name.endsWith('.zstd') ? 1 : 2)
|
|
183
|
+
return rank(left) - rank(right)
|
|
184
|
+
})
|
|
185
|
+
for (const name of ranked) {
|
|
186
|
+
try {
|
|
187
|
+
const file = join(sessionDir, name)
|
|
188
|
+
const line = name.endsWith('.zstd') ? await readZstdFirstLine(file) : await readFirstLine(file)
|
|
189
|
+
const parsed = JSON.parse(line)
|
|
190
|
+
if (parsed !== null && typeof parsed === 'object' && typeof parsed.cwd === 'string' && parsed.cwd !== '') {
|
|
191
|
+
return parsed.cwd
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
// Try the next generation/log file.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return null
|
|
198
|
+
}
|
|
199
|
+
|
|
65
200
|
/** Find `<sessionsRoot>/<slug>/<sessionId>`; null when the session has no log. */
|
|
66
201
|
export async function findSessionDir(sessionsRoot, sessionId) {
|
|
67
202
|
let dirents
|
package/lib/version.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Single source of truth for the Host-reported plugin version.
|
|
3
|
-
*
|
|
4
|
-
* `package.json` stays authoritative for the package itself; this module is
|
|
5
|
-
* asserted against it by `test/manifest.test.mjs` so the two can never drift
|
|
6
|
-
* (they did: the constant stayed at 0.1.0 while the package reached 0.1.4).
|
|
7
|
-
*/
|
|
8
|
-
export const PLUGIN_VERSION = '0.3.
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the Host-reported plugin version.
|
|
3
|
+
*
|
|
4
|
+
* `package.json` stays authoritative for the package itself; this module is
|
|
5
|
+
* asserted against it by `test/manifest.test.mjs` so the two can never drift
|
|
6
|
+
* (they did: the constant stayed at 0.1.0 while the package reached 0.1.4).
|
|
7
|
+
*/
|
|
8
|
+
export const PLUGIN_VERSION = '0.3.13'
|
package/package.json
CHANGED
|
@@ -1,81 +1,81 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-conversation-recycle-bin",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "DSH conversation deletion with an optional, previewed cascade into conversation-associated memory: soft delete + recycle bin, audit trail, batch rate limits and per-session rollback. 删除对话(可选一并删除关联记忆):软删除与回收站、审计留痕、批量限流、失败回滚。",
|
|
5
|
-
"repository": {
|
|
6
|
-
"type": "git",
|
|
7
|
-
"url": "git+https://github.com/HERO476/dsh-conversation-recycle-bin.git"
|
|
8
|
-
},
|
|
9
|
-
"homepage": "https://github.com/HERO476/dsh-conversation-recycle-bin#readme",
|
|
10
|
-
"bugs": {
|
|
11
|
-
"url": "https://github.com/HERO476/dsh-conversation-recycle-bin/issues"
|
|
12
|
-
},
|
|
13
|
-
"author": "HERO476",
|
|
14
|
-
"type": "module",
|
|
15
|
-
"main": "lib/index.js",
|
|
16
|
-
"exports": {
|
|
17
|
-
".": "./lib/index.js",
|
|
18
|
-
"./client": "./client.js",
|
|
19
|
-
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
20
|
-
"./package.json": "./package.json",
|
|
21
|
-
"./icon": "./icon.svg"
|
|
22
|
-
},
|
|
23
|
-
"files": [
|
|
24
|
-
"lib",
|
|
25
|
-
"client.js",
|
|
26
|
-
"cordis.patch.yml",
|
|
27
|
-
"config.schema.json",
|
|
28
|
-
"icon.svg",
|
|
29
|
-
"README.md",
|
|
30
|
-
"INSTALL.md",
|
|
31
|
-
"CHANGELOG.md",
|
|
32
|
-
"AUDIT-2026-10-17-desktop-web.md",
|
|
33
|
-
"LICENSE"
|
|
34
|
-
],
|
|
35
|
-
"engines": {
|
|
36
|
-
"node": ">=20",
|
|
37
|
-
"dsh": ">=0.2.1-rc.2"
|
|
38
|
-
},
|
|
39
|
-
"optionalDependencies": {
|
|
40
|
-
"nodemailer": "^10.1.2"
|
|
41
|
-
},
|
|
42
|
-
"license": "MIT",
|
|
43
|
-
"keywords": [
|
|
44
|
-
"dsh",
|
|
45
|
-
"dsh-plugin",
|
|
46
|
-
"deepseek-harness",
|
|
47
|
-
"conversation",
|
|
48
|
-
"delete",
|
|
49
|
-
"session",
|
|
50
|
-
"memory",
|
|
51
|
-
"recycle-bin",
|
|
52
|
-
"audit"
|
|
53
|
-
],
|
|
54
|
-
"icon": "./icon.svg",
|
|
55
|
-
"meta": {
|
|
56
|
-
"title": "删除对话",
|
|
57
|
-
"description": "删除对话并可选择一并删除该对话的关联记忆;支持软删除、回收站、审计、批量限流与失败回滚。"
|
|
58
|
-
},
|
|
59
|
-
"scripts": {
|
|
60
|
-
"test": "node test/manifest.test.mjs && node test/client-module.test.mjs && node test/core.test.mjs && node test/retention.test.mjs && node test/cascade.test.mjs && node test/memory.test.mjs && node test/trash-audit.test.mjs && node test/feedback.test.mjs && node test/api.test.mjs",
|
|
61
|
-
"check": "node scripts/check-syntax.mjs && node test/manifest.test.mjs"
|
|
62
|
-
},
|
|
63
|
-
"dsh": {
|
|
64
|
-
"manifestVersion": 1,
|
|
65
|
-
"bundle": {
|
|
66
|
-
"patch": "./cordis.patch.yml"
|
|
67
|
-
},
|
|
68
|
-
"client": {
|
|
69
|
-
"platform": "web",
|
|
70
|
-
"immediately": true,
|
|
71
|
-
"inject": [
|
|
72
|
-
"@deepseek-ai/dsh-client-locale",
|
|
73
|
-
"@deepseek-ai/dsh-client-connection",
|
|
74
|
-
"@deepseek-ai/dsh-client-ui-renderer",
|
|
75
|
-
"@deepseek-ai/dsh-client-ui-workspace",
|
|
76
|
-
"@deepseek-ai/dsh-client-ui-conversation",
|
|
77
|
-
"@deepseek-ai/dsh-client-ui-settings"
|
|
78
|
-
]
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-conversation-recycle-bin",
|
|
3
|
+
"version": "0.3.13",
|
|
4
|
+
"description": "DSH conversation deletion with an optional, previewed cascade into conversation-associated memory: soft delete + recycle bin, audit trail, batch rate limits and per-session rollback. 删除对话(可选一并删除关联记忆):软删除与回收站、审计留痕、批量限流、失败回滚。",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "git+https://github.com/HERO476/dsh-conversation-recycle-bin.git"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/HERO476/dsh-conversation-recycle-bin#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/HERO476/dsh-conversation-recycle-bin/issues"
|
|
12
|
+
},
|
|
13
|
+
"author": "HERO476",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "lib/index.js",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": "./lib/index.js",
|
|
18
|
+
"./client": "./client.js",
|
|
19
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
20
|
+
"./package.json": "./package.json",
|
|
21
|
+
"./icon": "./icon.svg"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"lib",
|
|
25
|
+
"client.js",
|
|
26
|
+
"cordis.patch.yml",
|
|
27
|
+
"config.schema.json",
|
|
28
|
+
"icon.svg",
|
|
29
|
+
"README.md",
|
|
30
|
+
"INSTALL.md",
|
|
31
|
+
"CHANGELOG.md",
|
|
32
|
+
"AUDIT-2026-10-17-desktop-web.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=20",
|
|
37
|
+
"dsh": ">=0.2.1-rc.2"
|
|
38
|
+
},
|
|
39
|
+
"optionalDependencies": {
|
|
40
|
+
"nodemailer": "^10.1.2"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"keywords": [
|
|
44
|
+
"dsh",
|
|
45
|
+
"dsh-plugin",
|
|
46
|
+
"deepseek-harness",
|
|
47
|
+
"conversation",
|
|
48
|
+
"delete",
|
|
49
|
+
"session",
|
|
50
|
+
"memory",
|
|
51
|
+
"recycle-bin",
|
|
52
|
+
"audit"
|
|
53
|
+
],
|
|
54
|
+
"icon": "./icon.svg",
|
|
55
|
+
"meta": {
|
|
56
|
+
"title": "删除对话",
|
|
57
|
+
"description": "删除对话并可选择一并删除该对话的关联记忆;支持软删除、回收站、审计、批量限流与失败回滚。"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"test": "node test/manifest.test.mjs && node test/client-module.test.mjs && node test/core.test.mjs && node test/retention.test.mjs && node test/cascade.test.mjs && node test/memory.test.mjs && node test/trash-audit.test.mjs && node test/feedback.test.mjs && node test/api.test.mjs",
|
|
61
|
+
"check": "node scripts/check-syntax.mjs && node test/manifest.test.mjs"
|
|
62
|
+
},
|
|
63
|
+
"dsh": {
|
|
64
|
+
"manifestVersion": 1,
|
|
65
|
+
"bundle": {
|
|
66
|
+
"patch": "./cordis.patch.yml"
|
|
67
|
+
},
|
|
68
|
+
"client": {
|
|
69
|
+
"platform": "web",
|
|
70
|
+
"immediately": true,
|
|
71
|
+
"inject": [
|
|
72
|
+
"@deepseek-ai/dsh-client-locale",
|
|
73
|
+
"@deepseek-ai/dsh-client-connection",
|
|
74
|
+
"@deepseek-ai/dsh-client-ui-renderer",
|
|
75
|
+
"@deepseek-ai/dsh-client-ui-workspace",
|
|
76
|
+
"@deepseek-ai/dsh-client-ui-conversation",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|