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",