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 CHANGED
@@ -37,8 +37,9 @@ dsh plugin add dsh-delete-conversation
37
37
 
38
38
  ## 安全边界
39
39
 
40
- - **正在运行的会话不能删。** 它有活跃的 Agent 持有写锁,且界面还指着它。
41
- 这种情况接口返回 `409`,弹窗里会提示你先结束它或改为归档。
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 an Agent currently owns this Session in this process.
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 live and must not be deleted.
176
+ * @returns true when the Session is running a turn and must not be deleted.
172
177
  */
173
- function sessionIsLive(ctx, sessionId) {
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) !== undefined;
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 live, or the log cannot be removed.
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 (sessionIsLive(ctx, sessionId)) {
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.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",