dsh-delete-conversation 1.0.0 → 1.0.1
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/README.md +3 -2
- package/lib/index.js +72 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -37,8 +37,9 @@ dsh plugin add dsh-delete-conversation
|
|
|
37
37
|
|
|
38
38
|
## 安全边界
|
|
39
39
|
|
|
40
|
-
-
|
|
41
|
-
|
|
40
|
+
- **正在跑的会话不能删。** 只有该会话此刻真的在跑一轮(Agent 状态为
|
|
41
|
+
`running`)才会被拒;打开过、停在空闲状态的会话可以正常删除。
|
|
42
|
+
这种情况接口返回 `409`,弹窗里会提示你先结束它。
|
|
42
43
|
- **只在同源、回环地址上的请求会被受理。** 带了 `forwarded` / `x-forwarded-for` /
|
|
43
44
|
`x-real-ip` 的转发请求一律 `403`,所以把这个端口暴露到公网也不会被人隔着隧道删会话。
|
|
44
45
|
- **每个会话 id 都按格式校验**(`session-<uuid>`),不合法直接拒。
|
package/lib/index.js
CHANGED
|
@@ -165,35 +165,92 @@ async function detachFromWorkspaces(ctx, sessionId) {
|
|
|
165
165
|
}
|
|
166
166
|
|
|
167
167
|
/**
|
|
168
|
-
* Whether
|
|
168
|
+
* Whether this Session is mid-turn right now.
|
|
169
|
+
*
|
|
170
|
+
* `ctx.agents.get(id)` answers "is this Session loaded", not "is it busy": a
|
|
171
|
+
* Session you merely opened stays registered with status `idle` for the life of
|
|
172
|
+
* the process. Only `running` means a turn is actually driving the Session, so
|
|
173
|
+
* that — and not mere presence — is what must refuse deletion.
|
|
169
174
|
* @param ctx - host context.
|
|
170
175
|
* @param sessionId - stored Session id.
|
|
171
|
-
* @returns true when the Session is
|
|
176
|
+
* @returns true when the Session is running a turn and must not be deleted.
|
|
172
177
|
*/
|
|
173
|
-
function
|
|
178
|
+
function sessionIsRunning(ctx, sessionId) {
|
|
174
179
|
const agents = ctx.get('agents');
|
|
175
180
|
if (agents === undefined) return false;
|
|
176
181
|
try {
|
|
177
|
-
return agents.get(sessionId)
|
|
182
|
+
return agents.get(sessionId)?.status === 'running';
|
|
178
183
|
} catch {
|
|
179
184
|
return false;
|
|
180
185
|
}
|
|
181
186
|
}
|
|
182
187
|
|
|
188
|
+
/**
|
|
189
|
+
* The live Session object for this id, when this process has it loaded.
|
|
190
|
+
* @param ctx - host context.
|
|
191
|
+
* @param sessionId - stored Session id.
|
|
192
|
+
* @returns the live Session, or undefined when it is cold.
|
|
193
|
+
*/
|
|
194
|
+
function liveSession(ctx, sessionId) {
|
|
195
|
+
try {
|
|
196
|
+
return ctx.get('sessions')?.get(sessionId);
|
|
197
|
+
} catch {
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Drop a loaded-but-idle Session from this process.
|
|
204
|
+
*
|
|
205
|
+
* DSH has no public "close this Session" call: a Session you merely opened stays
|
|
206
|
+
* registered (status `idle`) until the process exits, and that live copy is what
|
|
207
|
+
* would put the sidebar row back on the next refresh. So the deletion announces
|
|
208
|
+
* the official disposal edge first — which makes the persistence writer, the
|
|
209
|
+
* projection cache and the Session controller each close and forget their own
|
|
210
|
+
* state — and then removes the two registry entries that kept it visible.
|
|
211
|
+
* Both lookups are feature-detected: if a future DSH renames these internals the
|
|
212
|
+
* deletion still removes every stored file, and simply reports that the live
|
|
213
|
+
* copy could not be unloaded.
|
|
214
|
+
* @param ctx - host context.
|
|
215
|
+
* @param sessionId - stored Session id.
|
|
216
|
+
* @param session - the live Session object.
|
|
217
|
+
* @returns how many registry entries were removed.
|
|
218
|
+
*/
|
|
219
|
+
function unloadLiveSession(ctx, sessionId, session) {
|
|
220
|
+
try {
|
|
221
|
+
ctx.emit('session/disposed', session);
|
|
222
|
+
} catch (error) {
|
|
223
|
+
log('disposal notification failed', { sessionId, message: error instanceof Error ? error.message : String(error) });
|
|
224
|
+
}
|
|
225
|
+
let removed = 0;
|
|
226
|
+
for (const serviceName of ['sessions', 'agents']) {
|
|
227
|
+
try {
|
|
228
|
+
const store = ctx.get(serviceName)?.store;
|
|
229
|
+
if (store instanceof Map && store.delete(sessionId)) removed += 1;
|
|
230
|
+
} catch {}
|
|
231
|
+
}
|
|
232
|
+
return removed;
|
|
233
|
+
}
|
|
234
|
+
|
|
183
235
|
/**
|
|
184
236
|
* Permanently delete one stored Session.
|
|
185
237
|
* @param ctx - host context.
|
|
186
238
|
* @param sessionId - stored Session id.
|
|
187
239
|
* @returns a result describing what was removed.
|
|
188
|
-
* @throws {Error} when the id is malformed, the Session is
|
|
240
|
+
* @throws {Error} when the id is malformed, the Session is mid-turn, or the log cannot be removed.
|
|
189
241
|
*/
|
|
190
242
|
async function deleteSession(ctx, sessionId) {
|
|
191
243
|
if (!SESSION_ID_PATTERN.test(sessionId)) throw new Error('invalid session id');
|
|
192
|
-
if (
|
|
193
|
-
const error = new Error('
|
|
244
|
+
if (sessionIsRunning(ctx, sessionId)) {
|
|
245
|
+
const error = new Error('该会话正在运行,无法删除;请先结束它,或等它跑完。');
|
|
194
246
|
error.code = 'session-active';
|
|
195
247
|
throw error;
|
|
196
248
|
}
|
|
249
|
+
// Unload the live copy BEFORE touching files: the disposal edge makes the
|
|
250
|
+
// projection cache flush one last time, which would otherwise rewrite the
|
|
251
|
+
// cache file this deletion is about to remove.
|
|
252
|
+
const session = liveSession(ctx, sessionId);
|
|
253
|
+
const unloaded = session === undefined ? 0 : unloadLiveSession(ctx, sessionId, session);
|
|
197
254
|
const directories = sessionDirectories(sessionId);
|
|
198
255
|
const cacheFile = join(PROJCACHE_DIR, `${sessionId}.json`);
|
|
199
256
|
const hadCache = existsSync(cacheFile);
|
|
@@ -215,6 +272,14 @@ async function deleteSession(ctx, sessionId) {
|
|
|
215
272
|
}
|
|
216
273
|
}
|
|
217
274
|
const workspace = await detachFromWorkspaces(ctx, sessionId);
|
|
275
|
+
// Tell every open client to drop the row now. This is the same event the host
|
|
276
|
+
// broadcasts when a Session leaves the store, so the sidebar follows the
|
|
277
|
+
// deletion instead of waiting for the next baseline pull.
|
|
278
|
+
try {
|
|
279
|
+
ctx.emit('api-session/removed', sessionId);
|
|
280
|
+
} catch (error) {
|
|
281
|
+
log('removal broadcast failed', { sessionId, message: error instanceof Error ? error.message : String(error) });
|
|
282
|
+
}
|
|
218
283
|
log('session deleted', { sessionId, directories, cacheRemoved, workspace });
|
|
219
284
|
return { sessionId, directories, cacheRemoved, workspace, existed: true };
|
|
220
285
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-delete-conversation",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "DSH 插件:在侧边栏会话的「…」菜单里加一项「删除会话」,彻底删除该会话的日志、工作区归属与投影缓存。Adds a \"Delete session\" action to the DeepSeek Harness sidebar session menu that permanently removes the stored session.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./lib/index.js",
|