dsh-conversation-recycle-bin 0.3.5 → 0.3.6
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 +19 -0
- package/lib/host/gateway.js +41 -1
- package/lib/host/lifecycle.js +22 -5
- package/lib/version.js +1 -1
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,25 @@
|
|
|
3
3
|
All notable changes to `dsh-conversation-recycle-bin` are documented here.
|
|
4
4
|
The format follows Keep a Changelog; the project uses Semantic Versioning.
|
|
5
5
|
|
|
6
|
+
## [0.3.6] — 2026-10-24
|
|
7
|
+
|
|
8
|
+
对应 `AUDIT-2026-10-24-desktop-web-rc3.md` §7 的四条「建议动作」(A2 / A3 / A5 / A6)。同一节的 A1、A4、A7、A8 复核后判定为产品取舍(不申报未直接消费的 client 依赖、不导出 `Config` 以免设置页脱离非表单语义、归档读保留双源缓存、`missingSessionPaths` 保持成员判定),本版不改。
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **A3 `sessionController` 缺失时抛出的不是插件错误**:`lib/host/lifecycle.js` 新增 `peek()`,把 `ctx.get?.(name) ?? ctx[name]` 的整条退化读取留在 `try` 内。`??` 右侧的属性读会命中 cordis 的 Proxy trap(`cannot get property "sessionController" without inject`),异常类型不是 `DeleteError`,会绕过统一错误映射并让删除在归档前失败。现在 `sessionController` 明确视为可选服务:缺失时跳过 `cancel`,归档与 drain 检查照常执行。新增用例以抛异常的 getter 模拟裁剪组合,断言归档仍然发生。
|
|
13
|
+
- **A6 运行中判定永远落到 `inspect()`**:`lib/host/gateway.js` 的 `probeRunning()` 改为主探 `agents.get(id)?.status === 'running'`(与宿主 `dsh-api-session-controller` 列表行的判据一致),旧的 `Session.running/active/busy` 字段探测降为兜底。删除前置的「是否运行中」现在真正走廉价的实时探测,不必为每次判定重读会话。
|
|
14
|
+
- **A2 事件数列恒为 `—`**:`stat()` 从不返回 `eventCount`,旧代码读的正是这个不存在的字段。活跃会话改用 `Session.seq`(会话存储将其定义为日志长度,精确、纯数值读,不触发已废弃的 `snapshotEvents()`);已落盘的非活跃会话没有便宜的精确来源,继续返回 `null`、由 UI 显示 `—`,不拿投影游标冒充精确值。新增两个用例(活跃 = 精确值、非活跃 = `null`),并把测试夹具的 `stat()` 改成 0.2.1 的真实形状(去掉 `eventCount`),避免此前的假绿。
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **A5 `engines.dsh`**:`package.json` 声明 `"dsh": ">=0.2.1-rc.2"`(本插件的实测下限)。该键在安装/声明时由版本闸门读取,运行时不强制;不设上界,因为更晚的版本尚未验证。
|
|
19
|
+
|
|
20
|
+
### Maintenance
|
|
21
|
+
|
|
22
|
+
- 测试 138 → **141**(api 41 → 44:活跃/非活跃事件数、无 `sessionController` 的裁剪组合、agents 三态运行判定)。
|
|
23
|
+
- `lib/version.js` 与 `package.json` 同步到 0.3.6(`manifest` 用例守门)。
|
|
24
|
+
|
|
6
25
|
## [0.3.5] — 2026-10-22
|
|
7
26
|
|
|
8
27
|
对应 `AUDIT-2026-10-22-desktop-web-compat.md`:把 0.3.4 之后一直只存在于工作树的修复正式发布,并落地该报告 §9 的全部建议。
|
package/lib/host/gateway.js
CHANGED
|
@@ -186,6 +186,21 @@ export function createSessionGateway({ ctx, paths, now = Date.now }) {
|
|
|
186
186
|
* @returns {boolean|undefined}
|
|
187
187
|
*/
|
|
188
188
|
function probeRunning(sessionId) {
|
|
189
|
+
// Authoritative cheap probe first: the Host computes a list row's `running`
|
|
190
|
+
// flag from exactly this check (`api-session-controller`: `agents.get(id)
|
|
191
|
+
// ?.status === "running"`). The `Session` object handed out by `sessions.get`
|
|
192
|
+
// carries no `running`/`active`/`busy` field, so the legacy probe below can
|
|
193
|
+
// only ever answer `undefined` on current Hosts.
|
|
194
|
+
const agents = service('agents')
|
|
195
|
+
if (agents !== undefined && typeof agents.get === 'function') {
|
|
196
|
+
try {
|
|
197
|
+
const agent = agents.get(sessionId)
|
|
198
|
+
if (agent === undefined || agent === null) return false
|
|
199
|
+
if (typeof agent.status === 'string') return agent.status === 'running'
|
|
200
|
+
} catch {
|
|
201
|
+
// Fall through to the legacy field probe.
|
|
202
|
+
}
|
|
203
|
+
}
|
|
189
204
|
const sessions = service('sessions')
|
|
190
205
|
if (sessions === undefined || typeof sessions.get !== 'function') return undefined
|
|
191
206
|
let current
|
|
@@ -202,6 +217,31 @@ export function createSessionGateway({ ctx, paths, now = Date.now }) {
|
|
|
202
217
|
return undefined
|
|
203
218
|
}
|
|
204
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Event count when it is cheaply knowable, `null` otherwise.
|
|
222
|
+
*
|
|
223
|
+
* `sessionPersistence.stat()` reports `header`/`revision`/`sizeBytes` (and no
|
|
224
|
+
* `eventCount` on 0.2.1), so the old `snapshot.eventCount` read was dead. A
|
|
225
|
+
* live session does expose the count exactly: the session store defines `seq`
|
|
226
|
+
* as "always the log length" (`dsh-session` `get seq()`), and it is a plain
|
|
227
|
+
* number read, not a materialized `snapshotEvents()` (that API is deprecated).
|
|
228
|
+
* For a stored, non-live session there is no equivalent that avoids reading
|
|
229
|
+
* and parsing the whole log, so the field stays `null` instead of presenting
|
|
230
|
+
* a projection cursor as an exact count.
|
|
231
|
+
* @returns {number|null}
|
|
232
|
+
*/
|
|
233
|
+
function eventCountFor(sessionId, snapshot) {
|
|
234
|
+
if (typeof snapshot?.eventCount === 'number') return snapshot.eventCount
|
|
235
|
+
const sessions = service('sessions')
|
|
236
|
+
if (sessions === undefined || typeof sessions.get !== 'function') return null
|
|
237
|
+
try {
|
|
238
|
+
const seq = sessions.get(sessionId)?.seq
|
|
239
|
+
return typeof seq === 'number' && Number.isFinite(seq) && seq >= 0 ? seq : null
|
|
240
|
+
} catch {
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
205
245
|
async function workspaceFor(session) {
|
|
206
246
|
const workspaces = await workspaceEntities()
|
|
207
247
|
const byId = workspaces.find((workspace) => workspace.id === session.workspaceId)
|
|
@@ -251,7 +291,7 @@ export function createSessionGateway({ ctx, paths, now = Date.now }) {
|
|
|
251
291
|
pinned: fromList?.pinned ?? false,
|
|
252
292
|
createdAt: snapshot?.header?.createdAt ?? null,
|
|
253
293
|
updatedAt: fromList?.updatedAt ?? null,
|
|
254
|
-
events:
|
|
294
|
+
events: eventCountFor(sessionId, snapshot),
|
|
255
295
|
sizeBytes: typeof snapshot?.sizeBytes === 'number' ? snapshot.sizeBytes : null,
|
|
256
296
|
workspace: workspace === null ? null : { id: workspace.id, title: workspace.title, path: workspace.path, index: entity },
|
|
257
297
|
workspaceId: workspace?.id ?? null,
|
package/lib/host/lifecycle.js
CHANGED
|
@@ -20,13 +20,26 @@ import { DeleteError } from '../core/errors.js'
|
|
|
20
20
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
|
|
21
21
|
|
|
22
22
|
export function createLifecycleOps({ ctx, gateway, config, now = Date.now, delay = sleep }) {
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
/**
|
|
24
|
+
* Optional-service probe.
|
|
25
|
+
*
|
|
26
|
+
* The property read has to stay inside the `try`: `ctx[name]` goes through
|
|
27
|
+
* cordis' Proxy trap, which throws `cannot get property "x" without inject`
|
|
28
|
+
* for a service this composition never provided. `ctx.get?.(name)` alone
|
|
29
|
+
* resolves to `undefined` in that case, and the `??` fallback would then
|
|
30
|
+
* throw a foreign error type out of the delete cascade (why a failed `get`
|
|
31
|
+
* must not be repaired by reading the property outside the guard).
|
|
32
|
+
*/
|
|
33
|
+
function peek(name) {
|
|
25
34
|
try {
|
|
26
|
-
|
|
35
|
+
return ctx.get?.(name) ?? ctx[name]
|
|
27
36
|
} catch {
|
|
28
|
-
|
|
37
|
+
return undefined
|
|
29
38
|
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function service(name) {
|
|
42
|
+
const value = peek(name)
|
|
30
43
|
if (value === undefined) {
|
|
31
44
|
throw new DeleteError('E_INTERNAL', `The ${name} service is unavailable.`, {})
|
|
32
45
|
}
|
|
@@ -55,7 +68,11 @@ export function createLifecycleOps({ ctx, gateway, config, now = Date.now, delay
|
|
|
55
68
|
*/
|
|
56
69
|
async quiesce(session) {
|
|
57
70
|
const registry = service('workspaceRegistry')
|
|
58
|
-
|
|
71
|
+
// Optional on purpose: a composition without the controller still deletes
|
|
72
|
+
// the log, it just cannot cancel an in-flight turn first (the archive gate
|
|
73
|
+
// and the drain check below still hold). `peek` keeps that path from
|
|
74
|
+
// throwing a cordis error instead of reaching the archive step.
|
|
75
|
+
const controller = peek('sessionController')
|
|
59
76
|
const wasArchived = session.archived === true
|
|
60
77
|
const drainMs = Math.max(0, Number(config.liveDrainMs) || 0)
|
|
61
78
|
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-conversation-recycle-bin",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
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
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,7 +33,8 @@
|
|
|
33
33
|
"LICENSE"
|
|
34
34
|
],
|
|
35
35
|
"engines": {
|
|
36
|
-
"node": ">=20"
|
|
36
|
+
"node": ">=20",
|
|
37
|
+
"dsh": ">=0.2.1-rc.2"
|
|
37
38
|
},
|
|
38
39
|
"optionalDependencies": {
|
|
39
40
|
"nodemailer": "^10.1.2"
|