From e1e9a9455f938052f4423ec0255fae55f23fd057 Mon Sep 17 00:00:00 2001 From: modusensus Date: Mon, 21 Sep 2026 20:20:10 +0800 Subject: [PATCH] =?UTF-8?q?fix(summarize):=20=E7=BC=BA=20setDistillCursor?= =?UTF-8?q?=20=E6=97=B6=E9=99=8D=E7=BA=A7=E5=86=85=E5=AD=98=E6=B8=B8?= =?UTF-8?q?=E6=A0=87=E8=80=8C=E9=9D=9E=E6=8A=9B=E9=94=99=20(#274)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anans 复审留下的非阻塞建议第 1 条:第三方宿主用旧版 service 构造时, persistCursor 在方法缺失分支硬抛错,会把 #274 之前能正常工作的宿主 变成每轮蒸馏即崩。降级为 warn + 内存游标(#274 前语义,本进程不重蒸, 跨重启重放由 saveWithDedupe 三元组兜底);方法存在但抛错仍向上传播, 恰一次回滚语义不动(对应回滚测试用例保持原样)。 补回归测试:缺方法时蒸馏照跑、记忆落库、不持久化、进程内不重蒸。 Anans 建议第 2 条(内存优先读)经评估不改:memoryDir 允许双宿主共用, DB 优先是跨进程游标一致的依据,内存优先会复活跨进程重复蒸馏。 --- dsh-mneme/lib/summarize.js | 7 ++++++- dsh-mneme/src/summarize.js | 7 ++++++- dsh-mneme/test/summarize.test.js | 24 ++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/dsh-mneme/lib/summarize.js b/dsh-mneme/lib/summarize.js index c4f4d88..7935a96 100644 --- a/dsh-mneme/lib/summarize.js +++ b/dsh-mneme/lib/summarize.js @@ -408,7 +408,12 @@ export function createSummarizer(ctx, service, config, deps = {}) { function persistCursor(sessionId, nextSeq) { if (!Number.isFinite(nextSeq)) return; if (typeof service.setDistillCursor !== "function") { - throw new Error("dsh-mneme: persistent summarization cursors are unavailable"); + // 第三方宿主拿旧版 service 构造时没有持久化游标能力:降级为内存游标 + // (#274 之前的行为),本进程内不重复蒸馏,重启后窗口重放由 + // saveWithDedupe 三元组兜底。方法存在但抛错仍向上传播——那是 + // 「写失败须回滚」的恰一次语义,不能吞(见 summarize.test.js 回滚用例)。 + ctx.logger?.warn?.("dsh-mneme: service.setDistillCursor unavailable, distill cursor falls back to in-memory"); + return; } service.setDistillCursor(sessionId, nextSeq); } diff --git a/dsh-mneme/src/summarize.js b/dsh-mneme/src/summarize.js index c4f4d88..7935a96 100644 --- a/dsh-mneme/src/summarize.js +++ b/dsh-mneme/src/summarize.js @@ -408,7 +408,12 @@ export function createSummarizer(ctx, service, config, deps = {}) { function persistCursor(sessionId, nextSeq) { if (!Number.isFinite(nextSeq)) return; if (typeof service.setDistillCursor !== "function") { - throw new Error("dsh-mneme: persistent summarization cursors are unavailable"); + // 第三方宿主拿旧版 service 构造时没有持久化游标能力:降级为内存游标 + // (#274 之前的行为),本进程内不重复蒸馏,重启后窗口重放由 + // saveWithDedupe 三元组兜底。方法存在但抛错仍向上传播——那是 + // 「写失败须回滚」的恰一次语义,不能吞(见 summarize.test.js 回滚用例)。 + ctx.logger?.warn?.("dsh-mneme: service.setDistillCursor unavailable, distill cursor falls back to in-memory"); + return; } service.setDistillCursor(sessionId, nextSeq); } diff --git a/dsh-mneme/test/summarize.test.js b/dsh-mneme/test/summarize.test.js index 61734ff..4b351ef 100644 --- a/dsh-mneme/test/summarize.test.js +++ b/dsh-mneme/test/summarize.test.js @@ -794,6 +794,30 @@ test("rolls back memory writes when persisting the cursor fails", async () => { assert.equal(store.getDistillCursor(session.id).last_seq, 2); }); +test("falls back to the in-memory cursor when service lacks setDistillCursor", async () => { + // 旧版 service(#274 之前)没有持久化游标:缺方法必须降级为内存游标照常蒸馏, + // 而不是抛错打断每轮蒸馏。方法存在但抛错走的是上一条回滚用例,两者不可混淆。 + const { events, store, service, calls } = setup( + { distillRateLimitIntervalMs: 0 }, + { stream: streamOf([{ type: "history", title: "降级游标", content: "缺方法也能蒸馏", importance: 3 }]) } + ); + delete service.setDistillCursor; // 模拟旧版 service:实例属性,直接删掉 + const handler = events.find((e) => e.name === "session/event").fn; + const session = { + id: "s-no-cursor-api", + requestHeader: () => ({ config: { provider: "deepseek", model: "deepseek-chat" } }), + events: [userMessage("旧宿主没有游标接口", 1), { seq: 2, type: "turn/end" }] + }; + + await handler(session, { seq: 2, type: "turn/end" }); + assert.equal(calls.length, 1, "distillation must still run without a persisted cursor"); + assert.equal(store.count(), 1, "the memory itself must be saved"); + assert.equal(store.getDistillCursor(session.id), undefined, "nothing may be persisted without the API"); + + await handler(session, { seq: 2, type: "turn/end" }); + assert.equal(calls.length, 1, "the in-memory cursor must prevent re-distillation in this process"); +}); + test("uses summarizeProvider/summarizeModel config override when set", async () => { const { events, calls } = setup({ summarizeProvider: "aliyun",