dsh-recall-plugin 2.3.13 → 2.3.14
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 +6 -0
- package/lib/snapshots.js +32 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
本文件格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循语义化版本。
|
|
4
4
|
|
|
5
|
+
## [2.3.14] - 2026-09-11
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
|
|
9
|
+
- **撤回 fork 出的子会话内,撤回预览对任何消息都误报「该消息是本会话中第一条用户消息」**:`resolveCutSeq` 冷读取走 `sessionQuery.readSession`,官方实现内部却用 `Session.create(events, header, inheritedEventCount)` 做回放校验——快照模式要求 seeded 头的 `inheritedEventCount` 恰等于 `log.length`(「seeded session constructor seed must equal its inherited prefix」),而读取面交给它的是全量逻辑日志(`snapshotEvents()` 无参=继承前缀+自身事件;冷读返回完整存储日志),两者必然不等,`readSession` 对任何 seeded 会话(本插件每次撤回 fork 出的子会话即此类)直接抛错;此前的 `catch` 把异常静默折成 null,与「真首条」不可分——于是子会话里点**任何**消息,面板都显示「该消息是本会话中第一条用户消息,无法回退对话;确认后仅回退项目文件」。修复:`resolveCutSeq` 增加降级链——`readSession` 抛错、或消息根本没出现在它给的事件里(读取面缺继承前缀的版本差异)时,改用 `sessionQuery.observeSession`(经 `Session.fromRestore` 恢复,restore 模式无该约束)读取全量逻辑日志;租约以 `Symbol.dispose` 显式释放(prepared 缓存项靠它减引用,漏释放会卡淘汰);只有「消息在、其前无 turn/end」才是可信的“首条”,两跳都失败才落到 null。`scanCutSeq` 拆出 `scanCutSeqDetail`(cut + found)供降级判断;契约补 `observeSession` 与 `SessionObservationLease`(可选——旧版 dsh 无此 API 时维持原行为)。**真机复验**(dsh web 0.1.5-rc.2 + POST `/api/recall/preview`):修前子会话 6 条消息 cutSeq 全 null、父会话正常;修后子会话 156/43/62/88/197、真首条(你好)仍 null、父会话不变。新增 `tests/unit/snapshots-cutseq.test.js`(6 例),compat-audit 新增 I33。插件功能、客户端与其余行为零变化。
|
|
10
|
+
|
|
5
11
|
## [2.3.13] - 2026-09-11
|
|
6
12
|
|
|
7
13
|
### 修复
|
package/lib/snapshots.js
CHANGED
|
@@ -52,7 +52,7 @@ function parseDiffOutput(text, isWin, maxChanges) {
|
|
|
52
52
|
const changes = all.slice(0, maxChanges);
|
|
53
53
|
return { changes, total: finalTotal, truncated: finalTotal > changes.length, treeId };
|
|
54
54
|
}
|
|
55
|
-
function
|
|
55
|
+
function scanCutSeqDetail(events, messageId) {
|
|
56
56
|
let anchor = -1;
|
|
57
57
|
for (let i = 0; i < events.length; i++) {
|
|
58
58
|
const e = events[i];
|
|
@@ -61,12 +61,21 @@ function scanCutSeq(events, messageId) {
|
|
|
61
61
|
break;
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
|
-
if (anchor < 0) return null;
|
|
64
|
+
if (anchor < 0) return { cut: null, found: false };
|
|
65
65
|
for (let i = anchor - 1; i >= 0; i--) {
|
|
66
66
|
const e = events[i];
|
|
67
|
-
if (e && e.type === "turn/end" && typeof e.seq === "number") return e.seq;
|
|
67
|
+
if (e && e.type === "turn/end" && typeof e.seq === "number") return { cut: e.seq, found: true };
|
|
68
68
|
}
|
|
69
|
-
return null;
|
|
69
|
+
return { cut: null, found: true };
|
|
70
|
+
}
|
|
71
|
+
function scanCutSeq(events, messageId) {
|
|
72
|
+
return scanCutSeqDetail(events, messageId).cut;
|
|
73
|
+
}
|
|
74
|
+
function disposeLease(lease) {
|
|
75
|
+
const disposeSymbol = Symbol.dispose;
|
|
76
|
+
if (!disposeSymbol || lease === null || typeof lease !== "object") return;
|
|
77
|
+
const dispose = lease[disposeSymbol];
|
|
78
|
+
if (typeof dispose === "function") dispose.call(lease);
|
|
70
79
|
}
|
|
71
80
|
async function rescueRollback(deps, opts) {
|
|
72
81
|
const { root, store, safetyId, safetyOk, rollbackError } = opts;
|
|
@@ -374,11 +383,26 @@ function createSnapshots(ctx, rt, config) {
|
|
|
374
383
|
} else {
|
|
375
384
|
const query = ctx.get("sessionQuery");
|
|
376
385
|
if (query) {
|
|
386
|
+
let fallback = false;
|
|
377
387
|
try {
|
|
378
388
|
const log = await query.readSession(sessionId);
|
|
379
|
-
|
|
389
|
+
const detail = scanCutSeqDetail(Array.isArray(log && log.events) ? log.events : [], messageId);
|
|
390
|
+
result = detail.cut;
|
|
391
|
+
fallback = !detail.found;
|
|
380
392
|
} catch (error) {
|
|
381
|
-
|
|
393
|
+
fallback = true;
|
|
394
|
+
}
|
|
395
|
+
if (fallback && typeof query.observeSession === "function") {
|
|
396
|
+
try {
|
|
397
|
+
const lease = await query.observeSession(sessionId);
|
|
398
|
+
try {
|
|
399
|
+
result = scanCutSeq(lease && Array.isArray(lease.events) ? lease.events : [], messageId);
|
|
400
|
+
} finally {
|
|
401
|
+
disposeLease(lease);
|
|
402
|
+
}
|
|
403
|
+
} catch (error) {
|
|
404
|
+
result = null;
|
|
405
|
+
}
|
|
382
406
|
}
|
|
383
407
|
}
|
|
384
408
|
}
|
|
@@ -396,5 +420,6 @@ export {
|
|
|
396
420
|
parseTagsWithTime,
|
|
397
421
|
parseTreeId,
|
|
398
422
|
rescueRollback,
|
|
399
|
-
scanCutSeq
|
|
423
|
+
scanCutSeq,
|
|
424
|
+
scanCutSeqDetail
|
|
400
425
|
};
|