dsh-native-session-delete 1.0.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/AGENTS.md +137 -0
- package/LICENSE +21 -0
- package/README.en.md +142 -0
- package/README.md +128 -0
- package/SECURITY.md +15 -0
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/compatibility.json +12 -0
- package/cordis.patch.yml +7 -0
- package/lib/client.js +2532 -0
- package/package.json +132 -0
- package/scripts/build-client.mjs +170 -0
- package/scripts/smoke-ui.mjs +216 -0
- package/src/host/delete-session.mjs +405 -0
- package/src/index.js +43 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
import { lstat, mkdtemp, realpath, rename, rm } from 'node:fs/promises'
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
3
|
+
|
|
4
|
+
const failure = (code, message) => ({ ok: false, error: { code, message } })
|
|
5
|
+
const MAX_REQUEST_BYTES = 8 * 1024
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Retain the lifecycle capabilities returned by the public AgentRegistry API.
|
|
9
|
+
* DSH intentionally exposes only a bare Agent from get(); deletion needs the
|
|
10
|
+
* original handle so the host can cancel, drain, unregister, and detach in its
|
|
11
|
+
* own supported order.
|
|
12
|
+
*/
|
|
13
|
+
export function installAgentHandleTracker(agents, sessions) {
|
|
14
|
+
const handles = new Map()
|
|
15
|
+
const reservations = new Set()
|
|
16
|
+
const originalCreate = agents.create
|
|
17
|
+
const originalResume = agents.resume
|
|
18
|
+
const originalAgentEnter = agents.enter
|
|
19
|
+
const originalSessionEnter = sessions?.enter
|
|
20
|
+
|
|
21
|
+
const assertAvailable = (sessionId) => {
|
|
22
|
+
if (typeof sessionId === 'string' && reservations.has(sessionId)) {
|
|
23
|
+
throw new Error(`session "${sessionId}" is being permanently deleted`)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const track = (handle) => {
|
|
28
|
+
if (handle?.agent?.id !== undefined && typeof handle.dispose === 'function') {
|
|
29
|
+
handles.set(handle.agent.id, handle)
|
|
30
|
+
}
|
|
31
|
+
return handle
|
|
32
|
+
}
|
|
33
|
+
const wrappedCreate = async function (...args) {
|
|
34
|
+
assertAvailable(args[0]?.sessionId)
|
|
35
|
+
return track(await Reflect.apply(originalCreate, this, args))
|
|
36
|
+
}
|
|
37
|
+
const wrappedResume = async function (...args) {
|
|
38
|
+
assertAvailable(args[0]?.resumeSessionId)
|
|
39
|
+
return track(await Reflect.apply(originalResume, this, args))
|
|
40
|
+
}
|
|
41
|
+
const wrappedAgentEnter = typeof originalAgentEnter === 'function'
|
|
42
|
+
? function (...args) {
|
|
43
|
+
assertAvailable(args[0]?.id)
|
|
44
|
+
return Reflect.apply(originalAgentEnter, this, args)
|
|
45
|
+
}
|
|
46
|
+
: undefined
|
|
47
|
+
const wrappedSessionEnter = typeof originalSessionEnter === 'function'
|
|
48
|
+
? function (...args) {
|
|
49
|
+
assertAvailable(args[0]?.id)
|
|
50
|
+
return Reflect.apply(originalSessionEnter, this, args)
|
|
51
|
+
}
|
|
52
|
+
: undefined
|
|
53
|
+
|
|
54
|
+
agents.create = wrappedCreate
|
|
55
|
+
agents.resume = wrappedResume
|
|
56
|
+
if (wrappedAgentEnter !== undefined) agents.enter = wrappedAgentEnter
|
|
57
|
+
if (wrappedSessionEnter !== undefined) sessions.enter = wrappedSessionEnter
|
|
58
|
+
let releaseStarted = false
|
|
59
|
+
let released = false
|
|
60
|
+
let resolveRelease
|
|
61
|
+
let releaseTask
|
|
62
|
+
|
|
63
|
+
const finishRelease = () => {
|
|
64
|
+
if (!releaseStarted || released || reservations.size !== 0) return
|
|
65
|
+
released = true
|
|
66
|
+
handles.clear()
|
|
67
|
+
if (agents.create === wrappedCreate) agents.create = originalCreate
|
|
68
|
+
if (agents.resume === wrappedResume) agents.resume = originalResume
|
|
69
|
+
if (wrappedAgentEnter !== undefined && agents.enter === wrappedAgentEnter) agents.enter = originalAgentEnter
|
|
70
|
+
if (wrappedSessionEnter !== undefined && sessions.enter === wrappedSessionEnter) sessions.enter = originalSessionEnter
|
|
71
|
+
resolveRelease?.()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
reserve(sessionId) {
|
|
76
|
+
if (releaseStarted || reservations.has(sessionId)) return undefined
|
|
77
|
+
reservations.add(sessionId)
|
|
78
|
+
let active = true
|
|
79
|
+
return () => {
|
|
80
|
+
if (!active) return
|
|
81
|
+
active = false
|
|
82
|
+
reservations.delete(sessionId)
|
|
83
|
+
finishRelease()
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
async dispose(sessionId) {
|
|
87
|
+
const handle = handles.get(sessionId)
|
|
88
|
+
if (handle === undefined || agents.get(sessionId) !== handle.agent) {
|
|
89
|
+
handles.delete(sessionId)
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
await handle.dispose()
|
|
93
|
+
handles.delete(sessionId)
|
|
94
|
+
return true
|
|
95
|
+
},
|
|
96
|
+
release() {
|
|
97
|
+
if (released) return releaseTask
|
|
98
|
+
if (!releaseStarted) {
|
|
99
|
+
releaseStarted = true
|
|
100
|
+
releaseTask = new Promise(resolve => { resolveRelease = resolve })
|
|
101
|
+
finishRelease()
|
|
102
|
+
}
|
|
103
|
+
return releaseTask
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const sendJson = (res, status, body) => {
|
|
109
|
+
res.writeHead(status, {
|
|
110
|
+
'content-type': 'application/json; charset=utf-8',
|
|
111
|
+
'cache-control': 'no-store',
|
|
112
|
+
'x-content-type-options': 'nosniff',
|
|
113
|
+
})
|
|
114
|
+
res.end(JSON.stringify(body))
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const readJsonBody = async (req) => {
|
|
118
|
+
const chunks = []
|
|
119
|
+
let size = 0
|
|
120
|
+
for await (const chunk of req) {
|
|
121
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)
|
|
122
|
+
size += buffer.length
|
|
123
|
+
if (size > MAX_REQUEST_BYTES) throw new Error('request-too-large')
|
|
124
|
+
chunks.push(buffer)
|
|
125
|
+
}
|
|
126
|
+
return JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const sameHeader = (left, right) => (
|
|
130
|
+
left?.id === right?.id
|
|
131
|
+
&& left?.version === right?.version
|
|
132
|
+
&& left?.createdAt === right?.createdAt
|
|
133
|
+
&& left?.cwd === right?.cwd
|
|
134
|
+
&& left?.parentSession === right?.parentSession
|
|
135
|
+
&& left?.seedLength === right?.seedLength
|
|
136
|
+
&& left?.origin === right?.origin
|
|
137
|
+
&& (left?.delegationDepth ?? 0) === (right?.delegationDepth ?? 0)
|
|
138
|
+
&& left?.agentPreset === right?.agentPreset
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
const inside = (root, target) => {
|
|
142
|
+
const path = relative(root, target)
|
|
143
|
+
return path !== '' && !path.startsWith(`..${sep}`) && path !== '..' && !isAbsolute(path)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const missing = error => error?.code === 'ENOENT'
|
|
147
|
+
|
|
148
|
+
const sameFile = (left, right) => (
|
|
149
|
+
left.dev === right.dev
|
|
150
|
+
&& left.ino === right.ino
|
|
151
|
+
&& left.mode === right.mode
|
|
152
|
+
&& left.size === right.size
|
|
153
|
+
&& left.birthtimeNs === right.birthtimeNs
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
const locationPaths = (sessionRoot, location) => {
|
|
157
|
+
if (location?.kind !== 'jsonl' || typeof location.path !== 'string') return undefined
|
|
158
|
+
const root = resolve(sessionRoot)
|
|
159
|
+
const transcript = resolve(location.path)
|
|
160
|
+
if (!isAbsolute(root) || !isAbsolute(transcript) || !inside(root, transcript)) return undefined
|
|
161
|
+
const parts = relative(root, transcript).split(sep).filter(Boolean)
|
|
162
|
+
if (parts.length !== 3 || !['session.jsonl', 'session.jsonl.zstd'].includes(parts[2])) return undefined
|
|
163
|
+
return {
|
|
164
|
+
root,
|
|
165
|
+
projectDirectory: join(root, parts[0]),
|
|
166
|
+
sessionDirectory: join(root, parts[0], parts[1]),
|
|
167
|
+
transcript,
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const validateExistingLocation = async (paths) => {
|
|
172
|
+
const [root, project, directory, transcript] = await Promise.all([
|
|
173
|
+
realpath(paths.root),
|
|
174
|
+
lstat(paths.projectDirectory, { bigint: true }),
|
|
175
|
+
lstat(paths.sessionDirectory, { bigint: true }),
|
|
176
|
+
lstat(paths.transcript, { bigint: true }),
|
|
177
|
+
])
|
|
178
|
+
if (
|
|
179
|
+
!project.isDirectory()
|
|
180
|
+
|| project.isSymbolicLink()
|
|
181
|
+
|| !directory.isDirectory()
|
|
182
|
+
|| directory.isSymbolicLink()
|
|
183
|
+
|| !transcript.isFile()
|
|
184
|
+
|| transcript.isSymbolicLink()
|
|
185
|
+
) return undefined
|
|
186
|
+
|
|
187
|
+
const [resolvedProject, resolvedDirectory, resolvedTranscript] = await Promise.all([
|
|
188
|
+
realpath(paths.projectDirectory),
|
|
189
|
+
realpath(paths.sessionDirectory),
|
|
190
|
+
realpath(paths.transcript),
|
|
191
|
+
])
|
|
192
|
+
if (
|
|
193
|
+
!inside(root, resolvedProject)
|
|
194
|
+
|| !inside(root, resolvedDirectory)
|
|
195
|
+
|| dirname(resolvedTranscript) !== resolvedDirectory
|
|
196
|
+
) return undefined
|
|
197
|
+
return { root, resolvedDirectory, directory, transcript }
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const pathExists = async (path) => {
|
|
201
|
+
try {
|
|
202
|
+
await lstat(path)
|
|
203
|
+
return true
|
|
204
|
+
} catch (error) {
|
|
205
|
+
if (missing(error)) return false
|
|
206
|
+
throw error
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Permanently remove one JSONL session directory. A live Agent is first torn
|
|
212
|
+
* down through its owned host handle; persistence retirement is then awaited
|
|
213
|
+
* before the same path and identity checks used for a cold session.
|
|
214
|
+
*/
|
|
215
|
+
export async function deleteSessionSafely(deps, { sessionRoot, sessionId }) {
|
|
216
|
+
if (!deps.sessionPersistence.supportsRawArtifacts) {
|
|
217
|
+
return failure('unsupported-backend', '当前会话存储不是可逐会话删除的 JSONL 后端。')
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const releaseReservation = deps.agentHandles.reserve?.(sessionId)
|
|
221
|
+
if (typeof deps.agentHandles.reserve === 'function' && releaseReservation === undefined) {
|
|
222
|
+
return failure('deletion-in-progress', '该会话正在删除中,请等待当前操作完成。')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
return await deleteReservedSession(deps, { sessionRoot, sessionId })
|
|
227
|
+
} finally {
|
|
228
|
+
releaseReservation?.()
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
async function deleteReservedSession(deps, { sessionRoot, sessionId }) {
|
|
233
|
+
let header = (await deps.sessionPersistence.list()).find(item => item.id === sessionId)
|
|
234
|
+
?? deps.sessions.get(sessionId)?.header
|
|
235
|
+
if (header === undefined) return failure('session-not-found', '会话不存在或已经删除。')
|
|
236
|
+
|
|
237
|
+
if (deps.agents.get(sessionId) !== undefined) {
|
|
238
|
+
let disposed
|
|
239
|
+
try {
|
|
240
|
+
disposed = await deps.agentHandles.dispose(sessionId)
|
|
241
|
+
} catch {
|
|
242
|
+
return failure('lifecycle-error', 'DSH 未能安全停止并摘载该会话,未删除任何内容。')
|
|
243
|
+
}
|
|
244
|
+
if (!disposed) {
|
|
245
|
+
return failure('lifecycle-unavailable', '无法取得该会话的宿主生命周期句柄,未删除任何内容。请确认插件已更新并在更新后重启 DeepSeek Harness。')
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (deps.sessions.get(sessionId) !== undefined || deps.agents.get(sessionId) !== undefined) {
|
|
250
|
+
return failure('lifecycle-unavailable', '宿主未能完整摘载该会话,未删除任何内容。')
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// inspect() waits for the persistence backend's asynchronous retirement
|
|
254
|
+
// drain. A never-materialized blank session is already fully removed here.
|
|
255
|
+
try {
|
|
256
|
+
const inspected = await deps.sessionPersistence.inspect(sessionId)
|
|
257
|
+
if (!sameHeader(header, inspected.meta)) {
|
|
258
|
+
return failure('unsafe-location', '会话在摘载期间发生了身份变化,未删除任何文件。')
|
|
259
|
+
}
|
|
260
|
+
header = inspected.meta
|
|
261
|
+
} catch (error) {
|
|
262
|
+
const stillStored = (await deps.sessionPersistence.list()).some(item => item.id === sessionId)
|
|
263
|
+
if (!stillStored && deps.sessions.get(sessionId) === undefined && deps.agents.get(sessionId) === undefined) {
|
|
264
|
+
const missingPaths = locationPaths(sessionRoot, deps.sessionPersistence.locate(header))
|
|
265
|
+
if (missingPaths !== undefined && !await pathExists(missingPaths.sessionDirectory)) {
|
|
266
|
+
return { ok: true, value: { deleted: true } }
|
|
267
|
+
}
|
|
268
|
+
return failure('storage-state-unknown', 'DSH 已摘载会话,但无法确认其存储是否已删除。')
|
|
269
|
+
}
|
|
270
|
+
return failure('storage-state-unknown', '无法确认会话存储状态,未继续删除。')
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const location = deps.sessionPersistence.locate(header)
|
|
274
|
+
const raw = await deps.sessionPersistence.readRaw(sessionId)
|
|
275
|
+
const paths = locationPaths(sessionRoot, location)
|
|
276
|
+
if (paths === undefined || raw === undefined || !sameHeader(header, raw.meta)) {
|
|
277
|
+
return failure('unsafe-location', '无法验证该会话的独立 JSONL 存储位置。')
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
let validated
|
|
281
|
+
try {
|
|
282
|
+
validated = await validateExistingLocation(paths)
|
|
283
|
+
} catch (error) {
|
|
284
|
+
if (error?.code === 'ENOENT') return failure('session-not-found', '会话不存在或已经删除。')
|
|
285
|
+
return failure('storage-error', '读取会话存储失败,未删除任何文件。')
|
|
286
|
+
}
|
|
287
|
+
if (validated === undefined) {
|
|
288
|
+
return failure('unsafe-location', '会话存储位置未通过安全校验,未删除任何文件。')
|
|
289
|
+
}
|
|
290
|
+
if (deps.sessions.get(sessionId) !== undefined || deps.agents.get(sessionId) !== undefined) {
|
|
291
|
+
return failure('session-reopened', '该会话在删除过程中被重新打开,已取消删除。')
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
let quarantineRoot
|
|
295
|
+
let quarantinedDirectory
|
|
296
|
+
let detached = false
|
|
297
|
+
try {
|
|
298
|
+
const moveDirectory = deps.moveDirectory ?? rename
|
|
299
|
+
quarantineRoot = await mkdtemp(join(dirname(validated.root), '.dsh-session-delete-'))
|
|
300
|
+
quarantinedDirectory = join(quarantineRoot, basename(validated.resolvedDirectory))
|
|
301
|
+
await moveDirectory(validated.resolvedDirectory, quarantinedDirectory)
|
|
302
|
+
detached = true
|
|
303
|
+
|
|
304
|
+
const quarantinedTranscript = join(quarantinedDirectory, basename(paths.transcript))
|
|
305
|
+
const [directoryAfter, transcriptAfter] = await Promise.all([
|
|
306
|
+
lstat(quarantinedDirectory, { bigint: true }),
|
|
307
|
+
lstat(quarantinedTranscript, { bigint: true }),
|
|
308
|
+
])
|
|
309
|
+
if (
|
|
310
|
+
directoryAfter.isSymbolicLink()
|
|
311
|
+
|| transcriptAfter.isSymbolicLink()
|
|
312
|
+
|| !sameFile(validated.directory, directoryAfter)
|
|
313
|
+
|| !sameFile(validated.transcript, transcriptAfter)
|
|
314
|
+
) {
|
|
315
|
+
await moveDirectory(quarantinedDirectory, validated.resolvedDirectory)
|
|
316
|
+
detached = false
|
|
317
|
+
await rm(quarantineRoot, { recursive: true, force: true })
|
|
318
|
+
return failure('unsafe-location', '会话目录在删除期间发生了身份变化,未删除任何文件。')
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const removeDirectory = deps.removeDirectory
|
|
322
|
+
?? (directory => rm(directory, { recursive: true, force: false }))
|
|
323
|
+
await removeDirectory(quarantinedDirectory)
|
|
324
|
+
if (await pathExists(quarantinedDirectory)) {
|
|
325
|
+
return failure('storage-partial', '会话已从 DSH 摘载,但存储清理未完成,不能确认永久删除成功。')
|
|
326
|
+
}
|
|
327
|
+
await rm(quarantineRoot, { recursive: true, force: true })
|
|
328
|
+
return { ok: true, value: { deleted: true } }
|
|
329
|
+
} catch (error) {
|
|
330
|
+
if (detached && quarantinedDirectory !== undefined) {
|
|
331
|
+
let remains = true
|
|
332
|
+
try {
|
|
333
|
+
remains = await pathExists(quarantinedDirectory)
|
|
334
|
+
} catch {
|
|
335
|
+
// Inaccessible storage is not proof of cleanup; retain the quarantine.
|
|
336
|
+
}
|
|
337
|
+
if (remains) {
|
|
338
|
+
return failure('storage-partial', '会话已从 DSH 摘载,但存储清理未完成,不能确认永久删除成功。')
|
|
339
|
+
}
|
|
340
|
+
if (quarantineRoot !== undefined) await rm(quarantineRoot, { recursive: true, force: true }).catch(() => undefined)
|
|
341
|
+
return { ok: true, value: { deleted: true } }
|
|
342
|
+
}
|
|
343
|
+
if (quarantineRoot !== undefined) await rm(quarantineRoot, { recursive: true, force: true }).catch(() => undefined)
|
|
344
|
+
if (missing(error)) return failure('session-not-found', '会话不存在或已经删除。')
|
|
345
|
+
return failure('storage-error', '存储删除失败,未删除任何文件。')
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Kept as an import-compatible alias for 0.1.0 consumers.
|
|
350
|
+
export const deleteColdSession = deleteSessionSafely
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* HTTP boundary for the destructive operation. The native client must make a
|
|
354
|
+
* same-origin JSON POST and include a confirmation-only header that ordinary
|
|
355
|
+
* links and HTML forms cannot add.
|
|
356
|
+
*/
|
|
357
|
+
export function createDeleteRequestHandler({ deleteSession }) {
|
|
358
|
+
return async (req, res) => {
|
|
359
|
+
const host = req.headers.host
|
|
360
|
+
const origin = req.headers.origin
|
|
361
|
+
const contentType = req.headers['content-type']
|
|
362
|
+
const confirmation = req.headers['x-dsh-session-delete-confirmation']
|
|
363
|
+
|
|
364
|
+
if (req.method !== 'POST') {
|
|
365
|
+
sendJson(res, 405, failure('method-not-allowed', '只允许使用 POST 删除会话。'))
|
|
366
|
+
return
|
|
367
|
+
}
|
|
368
|
+
if (
|
|
369
|
+
typeof host !== 'string'
|
|
370
|
+
|| origin !== `http://${host}`
|
|
371
|
+
|| confirmation !== 'delete-session'
|
|
372
|
+
) {
|
|
373
|
+
sendJson(res, 403, failure('forbidden', '删除请求未通过同源与确认校验。'))
|
|
374
|
+
return
|
|
375
|
+
}
|
|
376
|
+
if (typeof contentType !== 'string' || !contentType.toLowerCase().startsWith('application/json')) {
|
|
377
|
+
sendJson(res, 415, failure('unsupported-media-type', '删除请求必须使用 JSON。'))
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let body
|
|
382
|
+
try {
|
|
383
|
+
body = await readJsonBody(req)
|
|
384
|
+
} catch (error) {
|
|
385
|
+
const tooLarge = error instanceof Error && error.message === 'request-too-large'
|
|
386
|
+
sendJson(
|
|
387
|
+
res,
|
|
388
|
+
tooLarge ? 413 : 400,
|
|
389
|
+
failure(tooLarge ? 'request-too-large' : 'invalid-json', tooLarge ? '删除请求过大。' : '删除请求不是有效 JSON。'),
|
|
390
|
+
)
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
if (typeof body?.sessionId !== 'string' || body.sessionId.length === 0 || body.sessionId.length > 512) {
|
|
394
|
+
sendJson(res, 400, failure('invalid-session-id', '会话 ID 无效。'))
|
|
395
|
+
return
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
try {
|
|
399
|
+
const result = await deleteSession(body.sessionId)
|
|
400
|
+
sendJson(res, result.ok ? 200 : 409, result)
|
|
401
|
+
} catch {
|
|
402
|
+
sendJson(res, 500, failure('internal', '删除过程中发生未预期错误,未确认删除成功。'))
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDeleteRequestHandler,
|
|
3
|
+
deleteSessionSafely,
|
|
4
|
+
installAgentHandleTracker,
|
|
5
|
+
} from './host/delete-session.mjs'
|
|
6
|
+
|
|
7
|
+
export const name = 'dsh-session-delete'
|
|
8
|
+
export const inject = ['webServer', 'sessionPersistence', 'sessions', 'agents']
|
|
9
|
+
|
|
10
|
+
export function apply(ctx) {
|
|
11
|
+
const sessionRoot = ctx.sessionPersistence?.root
|
|
12
|
+
if (typeof sessionRoot !== 'string' || sessionRoot.length === 0) {
|
|
13
|
+
throw new Error('dsh-session-delete requires the per-session JSONL persistence backend')
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const agentHandles = installAgentHandleTracker(ctx.agents, ctx.sessions)
|
|
17
|
+
ctx.effect(() => () => agentHandles.release(), 'dsh-session-delete: agent lifecycle tracking')
|
|
18
|
+
|
|
19
|
+
const handler = createDeleteRequestHandler({
|
|
20
|
+
deleteSession: sessionId => deleteSessionSafely({
|
|
21
|
+
sessions: ctx.sessions,
|
|
22
|
+
agents: ctx.agents,
|
|
23
|
+
agentHandles,
|
|
24
|
+
sessionPersistence: ctx.sessionPersistence,
|
|
25
|
+
}, { sessionRoot, sessionId }),
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
ctx.effect(
|
|
29
|
+
() => ctx.webServer.register({
|
|
30
|
+
kind: 'exact',
|
|
31
|
+
path: '/plugins/dsh-session-delete/delete',
|
|
32
|
+
handler,
|
|
33
|
+
}),
|
|
34
|
+
'dsh-session-delete: confirmed permanent deletion route',
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export {
|
|
39
|
+
createDeleteRequestHandler,
|
|
40
|
+
deleteColdSession,
|
|
41
|
+
deleteSessionSafely,
|
|
42
|
+
installAgentHandleTracker,
|
|
43
|
+
} from './host/delete-session.mjs'
|