@modusensus/dsh-mneme 0.7.20 → 0.7.21
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.en.md +7 -1
- package/README.md +5 -0
- package/lib/dream/sleep.js +29 -10
- package/lib/dream.js +55 -14
- package/package.json +1 -1
- package/src/dream/sleep.js +29 -10
- package/src/dream.js +55 -14
- package/test/reasoning-effort.test.js +114 -0
package/README.en.md
CHANGED
|
@@ -166,6 +166,7 @@ Every **background LLM call** (autoDream consolidation + summary, autoSummarize
|
|
|
166
166
|
|
|
167
167
|
| Version | Highlights |
|
|
168
168
|
|------|------|
|
|
169
|
+
| **v0.7.21** | Fixed autoDream/sleep effort fallback being dead code on the stream path (the catch-based retry from v0.7.16 never fired): dsh-llm rc.1 turns adapter-stage failures (incl. `UNSUPPORTED_REASONING_EFFORT`) into a terminal error finish chunk instead of a throw; `streamText` now captures the finish-chunk failure cause (`describeStreamFailure` normalizes `{code,message}`) + `withEffortFallback` gains a `getStreamError` accessor (retries without effort when rejected) + `runAuditedLlm` supports `spec.streamError` (audit `error_message` carries the real cause; `run.error` stays a stable `"llm failed"`); 688 tests green |
|
|
169
170
|
| **v0.7.20** | Heat model restored (issue #87): v0.7.0 self-evolving memory back (`src/heat.js` power-law decay `H=1/(1+λΔt)^α` + per-type half-lives), sleep demotion dual protection (cold time-window + heat<0.05 + importance<5), touchRecalled gating back on `heatEnabled`, entity heat projection (ego node heat → size/opacity), recall_runs recording on by default; **default OFF** (matches v0.7.12 behavior out of the box) + feature_flags whitelist rollback switch + lightMode linkage + sleep demotion audit counts exposed (workbench can show "N demoted") + phase-two frontend (/list heat projection, HeatBadge three-tier badge, self-gated status heat-distribution card, order=heat page-local sort, all self-gated); better-sidebar fix (issue #88: soft integration moved to an inner dynamic sub-plugin, no more startup failure without bs); 685 tests green |
|
|
170
171
|
| **v0.7.18** | Ecosystem step one + query convergence: better-sidebar soft integration (inject declaration + optional peer `dsh-better-sidebar` + registerTab reusing the four views, safe skip when absent; narrow-container `@container` adaptation) + `/list?deposited=only` view (receipt_chain ∪ source=dream) + deposited/archived filter chips in the library + status-page dashboard (server totals + view-all jumps) + drawer restore for archived memories; 667 tests green |
|
|
171
172
|
| **v0.7.17** | Panel polish: sidebar entry continuously aligned with the host (MutationObserver syncs the New-Session class + `width:100%` + native centering restored, resilient to async skin rewrites) + importance rendered as Lucide star glyphs (`ImportanceStars` filled/hollow component replacing text ★) + toolbar dropdown stacking fix (z-index moved to the container; export/import menu no longer painted under the sticky month header); 664 tests green |
|
|
@@ -212,7 +213,12 @@ Every **background LLM call** (autoDream consolidation + summary, autoSummarize
|
|
|
212
213
|
| v0.7.1–0.7.8 | ✅ Done | Issue fixes + graph backfill | tags↔entity_attrs bridge, inline-confirm delete, sidebar trigger toggle, brace escaping, user/fact layered types + stats endpoint (later removed), prefix-id resolution (later removed), sleep batch entity extraction, snapshotEvents() DSH compat; 764→812 tests green |
|
|
213
214
|
| v0.7.9 | ✅ Done | lib-sync gate | Issue #65: src-only shim silently killed the shipped lib — synced lib + pre-publish src↔lib consistency check + lib smoke tests; 815 tests green |
|
|
214
215
|
| v0.7.10–0.7.12 | ✅ Done | Panel redesign + near-rewrite | UX upgrades (color dots, pan/zoom, pagination, global search), then near-rewrite: inlined panel → pure HTTP API + standalone CLI + lightMode; TYPES narrowed 8→6 |
|
|
215
|
-
| v0.7.13–0.7.
|
|
216
|
+
| v0.7.13–0.7.15 | ✅ Done | Distillation, security, desktop | codingRetrospect + 429 governor; private reasoning blocks dropped (CWE-200); desktop panel redesign + feature flags + status dashboard + import/export |
|
|
217
|
+
| v0.7.16 | ✅ Done | autoDream thinking-model fix | config-first routing restored (Issue #25) + reasoningEffort auto-retry + honest audit on parse failure; backfilled API-route & lib smoke tests; 662 tests green |
|
|
218
|
+
| v0.7.17 | ✅ Done | Panel polish | Sidebar entry continuously aligned with the host (MutationObserver syncs the New-Session class + `width:100%` + native centering, resilient to async skin rewrites) + Lucide star glyphs (`ImportanceStars`) + toolbar dropdown stacking fix; 664 tests green |
|
|
219
|
+
| v0.7.18 | ✅ Done | Ecosystem step one + query convergence | better-sidebar soft integration (inject declaration + optional peer + registerTab reusing the four views, safe skip when absent; narrow-container `@container`) + `/list?deposited=only` view + deposited/archived filter chips + status-page dashboard + drawer restore; 667 tests green |
|
|
220
|
+
| v0.7.20 | ✅ Done | Heat restore + phase-two frontend + better-sidebar fix | Heat model fully restored (issue #87, backported from v0.7.10: power-law decay + TYPE_DECAY + sleep heat-combined dual protection + entity heat projection) + acceptance checklist landed (heatEnabled default OFF / feature_flags 31 keys / lightMode linkage / sleep demotion audit / updated_at⊥last_accessed_at contract) + phase-two frontend (/list heat projection, HeatBadge three-tier, order=heat page-local sort) + better-sidebar fix (issue #88: inner dynamic sub-plugin); 685 tests green |
|
|
221
|
+
| v0.7.21 | ✅ Done | effort-fallback stream fix | Catch-based effort fallback was dead code on the stream path (dsh-llm rc.1 turns adapter failures into a terminal error finish chunk instead of a throw) → `streamText` captures the finish-chunk cause (`describeStreamFailure`) + `withEffortFallback` gains a `getStreamError` accessor (retries without effort when rejected) + `runAuditedLlm` supports `spec.streamError` (real cause in audit); 688 tests green |
|
|
216
222
|
| **v0.8.0** | 🚧 Planned (late Sep) | Graph enhancement | Interest-drift visualization + scope isolation (issue #17) + cross-workspace sharing |
|
|
217
223
|
|
|
218
224
|
> All new capabilities ship as **toggleable features** (enabled/disabled via configuration), conservatively on by default and never breaking existing behavior. The `failure_memories` table and the autoDream decision engine have already paved the way for future reflective growth.
|
package/README.md
CHANGED
|
@@ -207,6 +207,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
207
207
|
|
|
208
208
|
| 版本 | 亮点 |
|
|
209
209
|
|------|------|
|
|
210
|
+
| **v0.7.21** | 修复 autoDream/sleep 的 effort 回退在流式路径失效(v0.7.16 的 catch 式回退是死代码):dsh-llm rc.1 把 adapter 阶段异常(含 `UNSUPPORTED_REASONING_EFFORT`)转成终态 error finish chunk 不再抛出;`streamText` 现捕获 finish-chunk 失败原因(新增 `describeStreamFailure` 归一化 `{code,message}`)+ `withEffortFallback` 增加 `getStreamError` 访问器(effort 被拒时去掉重试一次)+ `runAuditedLlm` 支持 `spec.streamError`(audit 行 `error_message` 携带真实原因,`run.error` 稳定 `"llm failed"` 不变);688 测试全绿 |
|
|
210
211
|
| **v0.7.20** | heat 热度模型回归(issue #87):找回 v0.7.0 自进化记忆(`src/heat.js` 幂律衰减 `H=1/(1+λΔt)^α` + per-type 差异化半衰期)、sleep 降级热联合双保护(时间窗冷 + heat<0.05 + importance<5)、touchRecalled 门控改回 `heatEnabled`、实体热投影(ego 节点 heat → 前端大小/明暗)、recall_runs 默认记录;**默认关**(默认=与 v0.7.12 行为一致)+ feature_flags 白名单回滚开关 + lightMode 联动 + sleep 降级审计计数暴露(工作动态可展示"降级 N 条")+ 阶段二前端(/list heat 投影、HeatBadge 三档徽章、状态页热度分布卡、order=heat 页内排序,全部自门控);better-sidebar 修复(issue #88:软集成改内层动态子插件,无 bs 环境不再启动失败);685 测试全绿 |
|
|
211
212
|
| **v0.7.18** | 生态第一步 + 查询收敛:better-sidebar 软集成(inject 声明 + optional peer `dsh-better-sidebar` + registerTab 复用四视图,未装安全跳过;窄容器 `@container` 适配)+ `/list?deposited=only` 沉淀视图(receipt_chain ∪ source=dream)+ 记忆库沉淀/已归档筛选 chip + 状态页仪表盘化(统计 + 查看全部跳转预置筛选)+ 抽屉归档记忆「恢复」;667 测试全绿 |
|
|
212
213
|
| **v0.7.17** | 面板体验细化:侧边栏入口持续对齐宿主(MutationObserver 同步「新会话」类名 + `width:100%` + 交还原生居中,皮肤异步改写不再失配)+ 重要性星级换 Lucide 星形(`ImportanceStars` 实心/空心组件,替换文本 ★)+ 工具栏下拉层级修复(z-index 提到容器,导出/导入菜单不再被吸顶月份头遮挡);664 测试全绿 |
|
|
@@ -286,6 +287,10 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
286
287
|
| **v0.7.14** | ✅ 完成 | 安全修复(CWE-200) | 蒸馏不再采集私有推理块:`collectMessages` 只采公开 text,防止记忆库沉淀模型私有思考链;617 测试全绿 |
|
|
287
288
|
| **v0.7.15** | ✅ 完成 | 桌面端适配 | 记忆库面板重设计 + 功能开关 30 键 UI(features API)+ 状态页工作台 + 导入导出(镜像同构 md 黄金闭环)+ Token 面板默认遮蔽;645 测试全绿 |
|
|
288
289
|
| **v0.7.16** | ✅ 完成 | autoDream thinking 模型空体修复 + 补测 | 恢复 config-first 路由(设置面板「巩固模型」生效,Issue #25)+ reasoningEffort 被拒自动去掉重试 + 解析失败如实记 llm_audit error;补测 API 路由空白(/delete、/entities、/external-api)+ lib 运行时冒烟;662 测试全绿 |
|
|
290
|
+
| **v0.7.17** | ✅ 完成 | 面板体验细化 | 侧边栏入口持续对齐宿主(MutationObserver 同步「新会话」类名 + `width:100%` + 交还原生居中,皮肤异步改写不再失配)+ 重要性星级换 Lucide 星形(`ImportanceStars` 实心/空心组件,替换文本 ★)+ 工具栏下拉层级修复(z-index 提到容器,导出/导入菜单不再被吸顶月份头遮挡);664 测试全绿 |
|
|
291
|
+
| **v0.7.18** | ✅ 完成 | 生态第一步 + 查询收敛 | better-sidebar 软集成(inject 声明 + optional peer `dsh-better-sidebar` + registerTab 复用四视图,未装安全跳过;窄容器 `@container` 适配)+ `/list?deposited=only` 沉淀视图(receipt_chain ∪ source=dream)+ 记忆库沉淀/已归档筛选 chip + 状态页仪表盘化(统计 + 查看全部跳转预置筛选)+ 抽屉归档记忆「恢复」;667 测试全绿 |
|
|
292
|
+
| **v0.7.20** | ✅ 完成 | heat 回归 + 阶段二前端 + better-sidebar 修复 | heat 热度模型完整找回(issue #87,v0.7.10 移植:幂律衰减 + TYPE_DECAY + sleep 热联合双保护 + 实体热投影)+ 验收清单落地(heatEnabled 默认关 / feature_flags 31 键 / lightMode 联动 / sleep 降级审计暴露 / updated_at⊥last_accessed_at 契约)+ 阶段二前端(/list heat 投影、HeatBadge 三档、order=heat 页内排序)+ better-sidebar 修复(issue #88:内层动态子插件);685 测试全绿 |
|
|
293
|
+
| **v0.7.21** | ✅ 完成 | effort 回退流式修复 | autoDream/sleep 的 catch 式 effort 回退在流式路径是死代码(dsh-llm rc.1 把 adapter 异常转成终态 error finish chunk 不再抛出)→ `streamText` 捕获 finish-chunk 失败原因(`describeStreamFailure` 归一化)+ `withEffortFallback` 增加 `getStreamError` 访问器(effort 被拒去重试)+ `runAuditedLlm` 支持 `spec.streamError`(audit 记真实原因);688 测试全绿 |
|
|
289
294
|
| **v0.8.0** | 🚧 计划中(9 月末) | 图谱增强 | 兴趣漂移可视化 + scope 隔离(issue #17)+ 跨 workspace 记忆共享 |
|
|
290
295
|
|
|
291
296
|
> 新能力一律做成**可开关的功能**(配置启用/关闭),默认保守开启、不破坏现有行为。`failure_memories` 表与 autoDream 决策引擎已为后续反思性成长铺好路。
|
package/lib/dream/sleep.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { randomUUID, createHash } from "node:crypto";
|
|
18
18
|
import { validateDecisions, applyDecisions } from "./decisions.js";
|
|
19
19
|
import { findPotentialConflicts } from "./clustering.js";
|
|
20
|
-
import { buildReceipt, withEffortFallback } from "../dream.js";
|
|
20
|
+
import { buildReceipt, describeStreamFailure, withEffortFallback } from "../dream.js";
|
|
21
21
|
import { computeHeat } from "../heat.js";
|
|
22
22
|
|
|
23
23
|
const SUMMARY_MAX = 120;
|
|
@@ -61,12 +61,17 @@ function parseJsonArray(text) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** Same stream consumption contract as dream.js. */
|
|
64
|
-
async function streamText(ctx, options) {
|
|
64
|
+
async function streamText(ctx, options, onStreamError) {
|
|
65
65
|
if (!ctx?.llm?.stream) return undefined;
|
|
66
66
|
let text = "";
|
|
67
67
|
for await (const chunk of ctx.llm.stream(options)) {
|
|
68
68
|
if (chunk.type === "text-delta" && typeof chunk.text === "string") text += chunk.text;
|
|
69
69
|
if (chunk.type === "finish" && (chunk.reason?.kind === "error" || chunk.reason?.kind === "aborted")) {
|
|
70
|
+
// Same rc.1 error-as-finish-chunk behavior as dream.js — surface the
|
|
71
|
+
// cause instead of discarding it.
|
|
72
|
+
if (typeof onStreamError === "function") {
|
|
73
|
+
try { onStreamError(chunk.reason); } catch { /* diagnostics only */ }
|
|
74
|
+
}
|
|
70
75
|
return undefined;
|
|
71
76
|
}
|
|
72
77
|
}
|
|
@@ -186,7 +191,10 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
186
191
|
`候选冲突:\nid=${p.a.id} | type=${p.a.type} | title=${p.a.title}\n${p.a.content}\n---\nid=${p.b.id} | type=${p.b.type} | title=${p.b.title}\n${p.b.content}\n(相似度 ${p.similarity.toFixed(2)})`
|
|
187
192
|
).join("\n\n");
|
|
188
193
|
const sleepEffort = config.sleepReasoningEffort && config.sleepReasoningEffort !== "none" ? config.sleepReasoningEffort : null;
|
|
189
|
-
|
|
194
|
+
let conflictStreamFailure = "";
|
|
195
|
+
const runConflict = (withEffort) => {
|
|
196
|
+
conflictStreamFailure = "";
|
|
197
|
+
return streamText(ctx, {
|
|
190
198
|
provider: route.provider,
|
|
191
199
|
model: route.model,
|
|
192
200
|
purpose: "sleep-conflict",
|
|
@@ -196,9 +204,13 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
196
204
|
{ role: "system", content: [{ type: "text", text: CONFLICT_PROMPT }] },
|
|
197
205
|
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
198
206
|
]
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
|
|
207
|
+
}, (reason) => { conflictStreamFailure = describeStreamFailure(reason); });
|
|
208
|
+
};
|
|
209
|
+
const text = await withEffortFallback(ctx, sleepEffort, () => runConflict(true), () => runConflict(false), () => conflictStreamFailure);
|
|
210
|
+
if (text === undefined) {
|
|
211
|
+
if (conflictStreamFailure) ctx.logger?.warn?.(`dsh-mneme sleep: conflict stream aborted or errored (${conflictStreamFailure})`);
|
|
212
|
+
return { status: "failed", error: "llm failed" };
|
|
213
|
+
}
|
|
202
214
|
const decisions = parseJsonArray(text);
|
|
203
215
|
if (!decisions) return { status: "failed", error: "invalid decisions json" };
|
|
204
216
|
// validateDecisions 要求每个 snapshot id 恰好被 claim 一次。v0.4.4 起它本身
|
|
@@ -298,7 +310,10 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
298
310
|
.join("\n");
|
|
299
311
|
const maxPatterns = config.sleepMaxPatternPerRun ?? 3;
|
|
300
312
|
const sleepEffort = config.sleepReasoningEffort && config.sleepReasoningEffort !== "none" ? config.sleepReasoningEffort : null;
|
|
301
|
-
|
|
313
|
+
let patternStreamFailure = "";
|
|
314
|
+
const runPattern = (withEffort) => {
|
|
315
|
+
patternStreamFailure = "";
|
|
316
|
+
return streamText(ctx, {
|
|
302
317
|
provider: route.provider,
|
|
303
318
|
model: route.model,
|
|
304
319
|
purpose: "sleep-pattern",
|
|
@@ -308,9 +323,13 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
308
323
|
{ role: "system", content: [{ type: "text", text: PATTERN_PROMPT.replace("N", String(maxPatterns)) }] },
|
|
309
324
|
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
310
325
|
]
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
|
|
326
|
+
}, (reason) => { patternStreamFailure = describeStreamFailure(reason); });
|
|
327
|
+
};
|
|
328
|
+
const text = await withEffortFallback(ctx, sleepEffort, () => runPattern(true), () => runPattern(false), () => patternStreamFailure);
|
|
329
|
+
if (text === undefined) {
|
|
330
|
+
if (patternStreamFailure) ctx.logger?.warn?.(`dsh-mneme sleep: pattern stream aborted or errored (${patternStreamFailure})`);
|
|
331
|
+
return { status: "failed", error: "llm failed" };
|
|
332
|
+
}
|
|
314
333
|
const decisions = parseJsonArray(text);
|
|
315
334
|
if (!decisions || decisions.length === 0) return { status: "skipped", reason: "no patterns found" };
|
|
316
335
|
// Evidence ids are provenance refs; an LLM-fabricated id would mint a dead
|
package/lib/dream.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { validateDecisions, applyDecisions } from "./dream/decisions.js";
|
|
2
2
|
import { clusterMemories, findPotentialConflicts } from "./dream/clustering.js";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
-
export { validateDecisions, applyDecisions, withEffortFallback };
|
|
4
|
+
export { validateDecisions, applyDecisions, withEffortFallback, describeStreamFailure };
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
// Extract the first JSON array from LLM output, tolerating markdown fences,
|
|
@@ -238,18 +238,34 @@ function buildRecordReceipts({ runId, committed, snapshot, policyEpoch }) {
|
|
|
238
238
|
* surfaces as undefined. The caller decides how to treat an empty result.
|
|
239
239
|
* `onUsage` (optional, Bug8) receives any usage chunk for token accounting.
|
|
240
240
|
*/
|
|
241
|
-
async function streamText(ctx, options, onUsage) {
|
|
241
|
+
async function streamText(ctx, options, onUsage, onStreamError) {
|
|
242
242
|
let text = "";
|
|
243
243
|
for await (const chunk of ctx.llm.stream(options)) {
|
|
244
244
|
if (chunk.type === "text-delta" && typeof chunk.text === "string") text += chunk.text;
|
|
245
245
|
if (chunk.type === "usage" && typeof onUsage === "function") onUsage(chunk);
|
|
246
246
|
if (chunk.type === "finish" && (chunk.reason?.kind === "error" || chunk.reason?.kind === "aborted")) {
|
|
247
|
+
// dsh-llm rc.1 turns adapter-stage failures (unknown provider route,
|
|
248
|
+
// UNSUPPORTED_REASONING_EFFORT from resolveCallWithInfo, …) into a
|
|
249
|
+
// terminal finish chunk instead of a throw — the cause rides in
|
|
250
|
+
// chunk.reason.failure {message, code}. Surface it, never swallow it.
|
|
251
|
+
if (typeof onStreamError === "function") {
|
|
252
|
+
try { onStreamError(chunk.reason); } catch { /* diagnostics only */ }
|
|
253
|
+
}
|
|
247
254
|
return undefined;
|
|
248
255
|
}
|
|
249
256
|
}
|
|
250
257
|
return text;
|
|
251
258
|
}
|
|
252
259
|
|
|
260
|
+
/** One-line human-readable cause from a finish-chunk failure reason. */
|
|
261
|
+
function describeStreamFailure(reason) {
|
|
262
|
+
const failure = reason?.failure ?? reason ?? {};
|
|
263
|
+
const code = failure.code ? String(failure.code) : "";
|
|
264
|
+
const message = String(failure.message ?? failure.error ?? "");
|
|
265
|
+
if (code && message) return message.includes(code) ? message : `${code}: ${message}`;
|
|
266
|
+
return code || message;
|
|
267
|
+
}
|
|
268
|
+
|
|
253
269
|
/**
|
|
254
270
|
* Bug8: wrap a background LLM call so its token/time/status are recorded in the
|
|
255
271
|
* llm_audit_logs table. Best-effort bookkeeping: a failure to WRITE the audit
|
|
@@ -279,9 +295,12 @@ async function runAuditedLlm(ctx, service, config, spec, body) {
|
|
|
279
295
|
});
|
|
280
296
|
if (result === undefined) {
|
|
281
297
|
// stream aborted/errored: the caller treats undefined as a failed run;
|
|
282
|
-
// record it as error here so the audit shows the truth.
|
|
298
|
+
// record it as error here so the audit shows the truth. spec.streamError
|
|
299
|
+
// (a getter) lets the caller attach the finish-chunk cause so the audit
|
|
300
|
+
// row names it instead of a bare "aborted".
|
|
283
301
|
status = "error";
|
|
284
|
-
|
|
302
|
+
const streamErr = typeof spec.streamError === "function" ? String(spec.streamError() ?? "") : "";
|
|
303
|
+
errorMessage = errorMessage ?? (streamErr ? `llm stream aborted or errored (${streamErr})` : "llm stream aborted or errored");
|
|
285
304
|
} else if (typeof spec.auditError === "function") {
|
|
286
305
|
// A stream that returned text but yields nothing usable is still a
|
|
287
306
|
// failed call — record it as error, not the default success, so the
|
|
@@ -328,10 +347,22 @@ async function runAuditedLlm(ctx, service, config, spec, body) {
|
|
|
328
347
|
* accepted → reasoning capped; rejected → provider default (old behavior),
|
|
329
348
|
* logged so the rejection is observable.
|
|
330
349
|
*/
|
|
331
|
-
async function withEffortFallback(ctx, effort, attempt, fallback) {
|
|
350
|
+
async function withEffortFallback(ctx, effort, attempt, fallback, getStreamError) {
|
|
332
351
|
if (!effort || effort === "none") return attempt();
|
|
333
352
|
try {
|
|
334
|
-
|
|
353
|
+
const result = await attempt();
|
|
354
|
+
if (result === undefined) {
|
|
355
|
+
// dsh-llm rc.1 streams a provider effort-rejection as a terminal error
|
|
356
|
+
// finish chunk (adapterStream catches everything, never throws) — match
|
|
357
|
+
// on the chunk's failure reason here or the retry below is dead code
|
|
358
|
+
// for the stream path.
|
|
359
|
+
const reason = String(getStreamError?.() ?? "");
|
|
360
|
+
if (/reasoning[\s_]*effort|UNSUPPORTED_REASONING_EFFORT/i.test(reason)) {
|
|
361
|
+
ctx.logger?.warn?.(`dsh-mneme dream: reasoningEffort "${effort}" rejected via stream (${reason}); retrying without it`);
|
|
362
|
+
return fallback();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return result;
|
|
335
366
|
} catch (error) {
|
|
336
367
|
const message = String(error?.message ?? error);
|
|
337
368
|
// matches both "reasoning effort" (natural language) and the bare
|
|
@@ -630,11 +661,15 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
630
661
|
// 记 audit error 并在日志带原始输出前 300 字节,便于定位"推理吞预算返回空体"。
|
|
631
662
|
const effort = config.dreamReasoningEffort && config.dreamReasoningEffort !== "none" ? config.dreamReasoningEffort : null;
|
|
632
663
|
let decisions = null;
|
|
633
|
-
|
|
664
|
+
let streamFailure = "";
|
|
665
|
+
const runConsolidation = (withEffort) => {
|
|
666
|
+
streamFailure = "";
|
|
667
|
+
return runAuditedLlm(ctx, service, config, {
|
|
634
668
|
triggerSource: "autoDream",
|
|
635
669
|
operationType: "dream_consolidate",
|
|
636
670
|
modelId: `${route.provider}:${route.model}`,
|
|
637
671
|
relatedMemoryIds: [...snapshot.keys()],
|
|
672
|
+
streamError: () => streamFailure,
|
|
638
673
|
auditError: (text) => {
|
|
639
674
|
decisions = extractJsonArray(text);
|
|
640
675
|
return Array.isArray(decisions) ? null : "no json array in llm output";
|
|
@@ -649,18 +684,19 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
649
684
|
{ role: "system", content: [{ type: "text", text: consolidationPrompt }] },
|
|
650
685
|
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
651
686
|
]
|
|
652
|
-
}, reportUsage));
|
|
687
|
+
}, reportUsage, (reason) => { streamFailure = describeStreamFailure(reason); }));
|
|
688
|
+
};
|
|
653
689
|
try {
|
|
654
690
|
// Bug8: the consolidation call is audited (tokens/time/status). A throw
|
|
655
691
|
// re-propagates to the catch below; an aborted stream returns undefined
|
|
656
692
|
// and is treated as a failed run after the check below.
|
|
657
|
-
decisionText = await withEffortFallback(ctx, effort, () => runConsolidation(true), () => runConsolidation(false));
|
|
693
|
+
decisionText = await withEffortFallback(ctx, effort, () => runConsolidation(true), () => runConsolidation(false), () => streamFailure);
|
|
658
694
|
} catch (error) {
|
|
659
695
|
logger?.warn?.(`dsh-mneme dream: consolidation llm call failed: ${String(error)}`);
|
|
660
696
|
return finish({ ok: false, error: "llm failed", summary: false });
|
|
661
697
|
}
|
|
662
698
|
if (decisionText === undefined) {
|
|
663
|
-
logger?.warn?.(
|
|
699
|
+
logger?.warn?.(`dsh-mneme dream: consolidation llm stream aborted or errored${streamFailure ? ` (${streamFailure})` : ""}`);
|
|
664
700
|
return finish({ ok: false, error: "llm failed", summary: false });
|
|
665
701
|
}
|
|
666
702
|
if (!Array.isArray(decisions)) {
|
|
@@ -782,11 +818,15 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
782
818
|
// Summary generation (second LLM call). A throwing stream is reported as
|
|
783
819
|
// a failed run; summary:false marks a run that produced no summary.
|
|
784
820
|
let summaryText;
|
|
785
|
-
|
|
821
|
+
let summaryStreamFailure = "";
|
|
822
|
+
const runSummary = (withEffort) => {
|
|
823
|
+
summaryStreamFailure = "";
|
|
824
|
+
return runAuditedLlm(ctx, service, config, {
|
|
786
825
|
triggerSource: "autoDream",
|
|
787
826
|
operationType: "dream_summarize",
|
|
788
827
|
modelId: `${route.provider}:${route.model}`,
|
|
789
|
-
relatedMemoryIds: []
|
|
828
|
+
relatedMemoryIds: [],
|
|
829
|
+
streamError: () => summaryStreamFailure
|
|
790
830
|
}, (reportUsage) => streamText(ctx, {
|
|
791
831
|
provider: route.provider,
|
|
792
832
|
model: route.model,
|
|
@@ -797,10 +837,11 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
797
837
|
{ role: "system", content: [{ type: "text", text: SUMMARY_PROMPT }] },
|
|
798
838
|
{ role: "user", content: [{ type: "text", text: service.all().filter((m) => !m.archived && m.type !== "summary").map((m) => `- ${m.title}: ${m.content}`).join("\n") }] }
|
|
799
839
|
]
|
|
800
|
-
}, reportUsage));
|
|
840
|
+
}, reportUsage, (reason) => { summaryStreamFailure = describeStreamFailure(reason); }));
|
|
841
|
+
};
|
|
801
842
|
try {
|
|
802
843
|
// Bug8: the summary call is audited too (operation dream_summarize).
|
|
803
|
-
summaryText = await withEffortFallback(ctx, effort, () => runSummary(true), () => runSummary(false));
|
|
844
|
+
summaryText = await withEffortFallback(ctx, effort, () => runSummary(true), () => runSummary(false), () => summaryStreamFailure);
|
|
804
845
|
} catch (error) {
|
|
805
846
|
logger?.warn?.(`dsh-mneme dream: summary llm call failed: ${String(error)}`);
|
|
806
847
|
return finish({ ok: false, error: "llm failed", applied, decisions: auditDecisions, outcome, frozen: frozenCount, summary: false });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
3
|
"description": "Cross-session memory plugin for DeepSeek Harness with autoDream consolidation: SQLite store, Markdown mirrors, 7 model tools, automatic injection, session summarization, user profile/rules, custom slash commands, vector (semantic) search, and a Web GUI panel",
|
|
4
|
-
"version": "0.7.
|
|
4
|
+
"version": "0.7.21",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
package/src/dream/sleep.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { randomUUID, createHash } from "node:crypto";
|
|
18
18
|
import { validateDecisions, applyDecisions } from "./decisions.js";
|
|
19
19
|
import { findPotentialConflicts } from "./clustering.js";
|
|
20
|
-
import { buildReceipt, withEffortFallback } from "../dream.js";
|
|
20
|
+
import { buildReceipt, describeStreamFailure, withEffortFallback } from "../dream.js";
|
|
21
21
|
import { computeHeat } from "../heat.js";
|
|
22
22
|
|
|
23
23
|
const SUMMARY_MAX = 120;
|
|
@@ -61,12 +61,17 @@ function parseJsonArray(text) {
|
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
/** Same stream consumption contract as dream.js. */
|
|
64
|
-
async function streamText(ctx, options) {
|
|
64
|
+
async function streamText(ctx, options, onStreamError) {
|
|
65
65
|
if (!ctx?.llm?.stream) return undefined;
|
|
66
66
|
let text = "";
|
|
67
67
|
for await (const chunk of ctx.llm.stream(options)) {
|
|
68
68
|
if (chunk.type === "text-delta" && typeof chunk.text === "string") text += chunk.text;
|
|
69
69
|
if (chunk.type === "finish" && (chunk.reason?.kind === "error" || chunk.reason?.kind === "aborted")) {
|
|
70
|
+
// Same rc.1 error-as-finish-chunk behavior as dream.js — surface the
|
|
71
|
+
// cause instead of discarding it.
|
|
72
|
+
if (typeof onStreamError === "function") {
|
|
73
|
+
try { onStreamError(chunk.reason); } catch { /* diagnostics only */ }
|
|
74
|
+
}
|
|
70
75
|
return undefined;
|
|
71
76
|
}
|
|
72
77
|
}
|
|
@@ -186,7 +191,10 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
186
191
|
`候选冲突:\nid=${p.a.id} | type=${p.a.type} | title=${p.a.title}\n${p.a.content}\n---\nid=${p.b.id} | type=${p.b.type} | title=${p.b.title}\n${p.b.content}\n(相似度 ${p.similarity.toFixed(2)})`
|
|
187
192
|
).join("\n\n");
|
|
188
193
|
const sleepEffort = config.sleepReasoningEffort && config.sleepReasoningEffort !== "none" ? config.sleepReasoningEffort : null;
|
|
189
|
-
|
|
194
|
+
let conflictStreamFailure = "";
|
|
195
|
+
const runConflict = (withEffort) => {
|
|
196
|
+
conflictStreamFailure = "";
|
|
197
|
+
return streamText(ctx, {
|
|
190
198
|
provider: route.provider,
|
|
191
199
|
model: route.model,
|
|
192
200
|
purpose: "sleep-conflict",
|
|
@@ -196,9 +204,13 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
196
204
|
{ role: "system", content: [{ type: "text", text: CONFLICT_PROMPT }] },
|
|
197
205
|
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
198
206
|
]
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
|
|
207
|
+
}, (reason) => { conflictStreamFailure = describeStreamFailure(reason); });
|
|
208
|
+
};
|
|
209
|
+
const text = await withEffortFallback(ctx, sleepEffort, () => runConflict(true), () => runConflict(false), () => conflictStreamFailure);
|
|
210
|
+
if (text === undefined) {
|
|
211
|
+
if (conflictStreamFailure) ctx.logger?.warn?.(`dsh-mneme sleep: conflict stream aborted or errored (${conflictStreamFailure})`);
|
|
212
|
+
return { status: "failed", error: "llm failed" };
|
|
213
|
+
}
|
|
202
214
|
const decisions = parseJsonArray(text);
|
|
203
215
|
if (!decisions) return { status: "failed", error: "invalid decisions json" };
|
|
204
216
|
// validateDecisions 要求每个 snapshot id 恰好被 claim 一次。v0.4.4 起它本身
|
|
@@ -298,7 +310,10 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
298
310
|
.join("\n");
|
|
299
311
|
const maxPatterns = config.sleepMaxPatternPerRun ?? 3;
|
|
300
312
|
const sleepEffort = config.sleepReasoningEffort && config.sleepReasoningEffort !== "none" ? config.sleepReasoningEffort : null;
|
|
301
|
-
|
|
313
|
+
let patternStreamFailure = "";
|
|
314
|
+
const runPattern = (withEffort) => {
|
|
315
|
+
patternStreamFailure = "";
|
|
316
|
+
return streamText(ctx, {
|
|
302
317
|
provider: route.provider,
|
|
303
318
|
model: route.model,
|
|
304
319
|
purpose: "sleep-pattern",
|
|
@@ -308,9 +323,13 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
308
323
|
{ role: "system", content: [{ type: "text", text: PATTERN_PROMPT.replace("N", String(maxPatterns)) }] },
|
|
309
324
|
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
310
325
|
]
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
|
|
326
|
+
}, (reason) => { patternStreamFailure = describeStreamFailure(reason); });
|
|
327
|
+
};
|
|
328
|
+
const text = await withEffortFallback(ctx, sleepEffort, () => runPattern(true), () => runPattern(false), () => patternStreamFailure);
|
|
329
|
+
if (text === undefined) {
|
|
330
|
+
if (patternStreamFailure) ctx.logger?.warn?.(`dsh-mneme sleep: pattern stream aborted or errored (${patternStreamFailure})`);
|
|
331
|
+
return { status: "failed", error: "llm failed" };
|
|
332
|
+
}
|
|
314
333
|
const decisions = parseJsonArray(text);
|
|
315
334
|
if (!decisions || decisions.length === 0) return { status: "skipped", reason: "no patterns found" };
|
|
316
335
|
// Evidence ids are provenance refs; an LLM-fabricated id would mint a dead
|
package/src/dream.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { validateDecisions, applyDecisions } from "./dream/decisions.js";
|
|
2
2
|
import { clusterMemories, findPotentialConflicts } from "./dream/clustering.js";
|
|
3
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
4
|
-
export { validateDecisions, applyDecisions, withEffortFallback };
|
|
4
|
+
export { validateDecisions, applyDecisions, withEffortFallback, describeStreamFailure };
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
// Extract the first JSON array from LLM output, tolerating markdown fences,
|
|
@@ -238,18 +238,34 @@ function buildRecordReceipts({ runId, committed, snapshot, policyEpoch }) {
|
|
|
238
238
|
* surfaces as undefined. The caller decides how to treat an empty result.
|
|
239
239
|
* `onUsage` (optional, Bug8) receives any usage chunk for token accounting.
|
|
240
240
|
*/
|
|
241
|
-
async function streamText(ctx, options, onUsage) {
|
|
241
|
+
async function streamText(ctx, options, onUsage, onStreamError) {
|
|
242
242
|
let text = "";
|
|
243
243
|
for await (const chunk of ctx.llm.stream(options)) {
|
|
244
244
|
if (chunk.type === "text-delta" && typeof chunk.text === "string") text += chunk.text;
|
|
245
245
|
if (chunk.type === "usage" && typeof onUsage === "function") onUsage(chunk);
|
|
246
246
|
if (chunk.type === "finish" && (chunk.reason?.kind === "error" || chunk.reason?.kind === "aborted")) {
|
|
247
|
+
// dsh-llm rc.1 turns adapter-stage failures (unknown provider route,
|
|
248
|
+
// UNSUPPORTED_REASONING_EFFORT from resolveCallWithInfo, …) into a
|
|
249
|
+
// terminal finish chunk instead of a throw — the cause rides in
|
|
250
|
+
// chunk.reason.failure {message, code}. Surface it, never swallow it.
|
|
251
|
+
if (typeof onStreamError === "function") {
|
|
252
|
+
try { onStreamError(chunk.reason); } catch { /* diagnostics only */ }
|
|
253
|
+
}
|
|
247
254
|
return undefined;
|
|
248
255
|
}
|
|
249
256
|
}
|
|
250
257
|
return text;
|
|
251
258
|
}
|
|
252
259
|
|
|
260
|
+
/** One-line human-readable cause from a finish-chunk failure reason. */
|
|
261
|
+
function describeStreamFailure(reason) {
|
|
262
|
+
const failure = reason?.failure ?? reason ?? {};
|
|
263
|
+
const code = failure.code ? String(failure.code) : "";
|
|
264
|
+
const message = String(failure.message ?? failure.error ?? "");
|
|
265
|
+
if (code && message) return message.includes(code) ? message : `${code}: ${message}`;
|
|
266
|
+
return code || message;
|
|
267
|
+
}
|
|
268
|
+
|
|
253
269
|
/**
|
|
254
270
|
* Bug8: wrap a background LLM call so its token/time/status are recorded in the
|
|
255
271
|
* llm_audit_logs table. Best-effort bookkeeping: a failure to WRITE the audit
|
|
@@ -279,9 +295,12 @@ async function runAuditedLlm(ctx, service, config, spec, body) {
|
|
|
279
295
|
});
|
|
280
296
|
if (result === undefined) {
|
|
281
297
|
// stream aborted/errored: the caller treats undefined as a failed run;
|
|
282
|
-
// record it as error here so the audit shows the truth.
|
|
298
|
+
// record it as error here so the audit shows the truth. spec.streamError
|
|
299
|
+
// (a getter) lets the caller attach the finish-chunk cause so the audit
|
|
300
|
+
// row names it instead of a bare "aborted".
|
|
283
301
|
status = "error";
|
|
284
|
-
|
|
302
|
+
const streamErr = typeof spec.streamError === "function" ? String(spec.streamError() ?? "") : "";
|
|
303
|
+
errorMessage = errorMessage ?? (streamErr ? `llm stream aborted or errored (${streamErr})` : "llm stream aborted or errored");
|
|
285
304
|
} else if (typeof spec.auditError === "function") {
|
|
286
305
|
// A stream that returned text but yields nothing usable is still a
|
|
287
306
|
// failed call — record it as error, not the default success, so the
|
|
@@ -328,10 +347,22 @@ async function runAuditedLlm(ctx, service, config, spec, body) {
|
|
|
328
347
|
* accepted → reasoning capped; rejected → provider default (old behavior),
|
|
329
348
|
* logged so the rejection is observable.
|
|
330
349
|
*/
|
|
331
|
-
async function withEffortFallback(ctx, effort, attempt, fallback) {
|
|
350
|
+
async function withEffortFallback(ctx, effort, attempt, fallback, getStreamError) {
|
|
332
351
|
if (!effort || effort === "none") return attempt();
|
|
333
352
|
try {
|
|
334
|
-
|
|
353
|
+
const result = await attempt();
|
|
354
|
+
if (result === undefined) {
|
|
355
|
+
// dsh-llm rc.1 streams a provider effort-rejection as a terminal error
|
|
356
|
+
// finish chunk (adapterStream catches everything, never throws) — match
|
|
357
|
+
// on the chunk's failure reason here or the retry below is dead code
|
|
358
|
+
// for the stream path.
|
|
359
|
+
const reason = String(getStreamError?.() ?? "");
|
|
360
|
+
if (/reasoning[\s_]*effort|UNSUPPORTED_REASONING_EFFORT/i.test(reason)) {
|
|
361
|
+
ctx.logger?.warn?.(`dsh-mneme dream: reasoningEffort "${effort}" rejected via stream (${reason}); retrying without it`);
|
|
362
|
+
return fallback();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return result;
|
|
335
366
|
} catch (error) {
|
|
336
367
|
const message = String(error?.message ?? error);
|
|
337
368
|
// matches both "reasoning effort" (natural language) and the bare
|
|
@@ -630,11 +661,15 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
630
661
|
// 记 audit error 并在日志带原始输出前 300 字节,便于定位"推理吞预算返回空体"。
|
|
631
662
|
const effort = config.dreamReasoningEffort && config.dreamReasoningEffort !== "none" ? config.dreamReasoningEffort : null;
|
|
632
663
|
let decisions = null;
|
|
633
|
-
|
|
664
|
+
let streamFailure = "";
|
|
665
|
+
const runConsolidation = (withEffort) => {
|
|
666
|
+
streamFailure = "";
|
|
667
|
+
return runAuditedLlm(ctx, service, config, {
|
|
634
668
|
triggerSource: "autoDream",
|
|
635
669
|
operationType: "dream_consolidate",
|
|
636
670
|
modelId: `${route.provider}:${route.model}`,
|
|
637
671
|
relatedMemoryIds: [...snapshot.keys()],
|
|
672
|
+
streamError: () => streamFailure,
|
|
638
673
|
auditError: (text) => {
|
|
639
674
|
decisions = extractJsonArray(text);
|
|
640
675
|
return Array.isArray(decisions) ? null : "no json array in llm output";
|
|
@@ -649,18 +684,19 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
649
684
|
{ role: "system", content: [{ type: "text", text: consolidationPrompt }] },
|
|
650
685
|
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
651
686
|
]
|
|
652
|
-
}, reportUsage));
|
|
687
|
+
}, reportUsage, (reason) => { streamFailure = describeStreamFailure(reason); }));
|
|
688
|
+
};
|
|
653
689
|
try {
|
|
654
690
|
// Bug8: the consolidation call is audited (tokens/time/status). A throw
|
|
655
691
|
// re-propagates to the catch below; an aborted stream returns undefined
|
|
656
692
|
// and is treated as a failed run after the check below.
|
|
657
|
-
decisionText = await withEffortFallback(ctx, effort, () => runConsolidation(true), () => runConsolidation(false));
|
|
693
|
+
decisionText = await withEffortFallback(ctx, effort, () => runConsolidation(true), () => runConsolidation(false), () => streamFailure);
|
|
658
694
|
} catch (error) {
|
|
659
695
|
logger?.warn?.(`dsh-mneme dream: consolidation llm call failed: ${String(error)}`);
|
|
660
696
|
return finish({ ok: false, error: "llm failed", summary: false });
|
|
661
697
|
}
|
|
662
698
|
if (decisionText === undefined) {
|
|
663
|
-
logger?.warn?.(
|
|
699
|
+
logger?.warn?.(`dsh-mneme dream: consolidation llm stream aborted or errored${streamFailure ? ` (${streamFailure})` : ""}`);
|
|
664
700
|
return finish({ ok: false, error: "llm failed", summary: false });
|
|
665
701
|
}
|
|
666
702
|
if (!Array.isArray(decisions)) {
|
|
@@ -782,11 +818,15 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
782
818
|
// Summary generation (second LLM call). A throwing stream is reported as
|
|
783
819
|
// a failed run; summary:false marks a run that produced no summary.
|
|
784
820
|
let summaryText;
|
|
785
|
-
|
|
821
|
+
let summaryStreamFailure = "";
|
|
822
|
+
const runSummary = (withEffort) => {
|
|
823
|
+
summaryStreamFailure = "";
|
|
824
|
+
return runAuditedLlm(ctx, service, config, {
|
|
786
825
|
triggerSource: "autoDream",
|
|
787
826
|
operationType: "dream_summarize",
|
|
788
827
|
modelId: `${route.provider}:${route.model}`,
|
|
789
|
-
relatedMemoryIds: []
|
|
828
|
+
relatedMemoryIds: [],
|
|
829
|
+
streamError: () => summaryStreamFailure
|
|
790
830
|
}, (reportUsage) => streamText(ctx, {
|
|
791
831
|
provider: route.provider,
|
|
792
832
|
model: route.model,
|
|
@@ -797,10 +837,11 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
797
837
|
{ role: "system", content: [{ type: "text", text: SUMMARY_PROMPT }] },
|
|
798
838
|
{ role: "user", content: [{ type: "text", text: service.all().filter((m) => !m.archived && m.type !== "summary").map((m) => `- ${m.title}: ${m.content}`).join("\n") }] }
|
|
799
839
|
]
|
|
800
|
-
}, reportUsage));
|
|
840
|
+
}, reportUsage, (reason) => { summaryStreamFailure = describeStreamFailure(reason); }));
|
|
841
|
+
};
|
|
801
842
|
try {
|
|
802
843
|
// Bug8: the summary call is audited too (operation dream_summarize).
|
|
803
|
-
summaryText = await withEffortFallback(ctx, effort, () => runSummary(true), () => runSummary(false));
|
|
844
|
+
summaryText = await withEffortFallback(ctx, effort, () => runSummary(true), () => runSummary(false), () => summaryStreamFailure);
|
|
804
845
|
} catch (error) {
|
|
805
846
|
logger?.warn?.(`dsh-mneme dream: summary llm call failed: ${String(error)}`);
|
|
806
847
|
return finish({ ok: false, error: "llm failed", applied, decisions: auditDecisions, outcome, frozen: frozenCount, summary: false });
|
|
@@ -234,3 +234,117 @@ test("issue#9: sleep forwards sleepReasoningEffort on its LLM passes", async ()
|
|
|
234
234
|
}
|
|
235
235
|
store.close();
|
|
236
236
|
});
|
|
237
|
+
|
|
238
|
+
// ------------------------------------------------------------------ stream-level rejection
|
|
239
|
+
// dsh-llm rc.1 converts adapter-stage failures (including the provider's
|
|
240
|
+
// UNSUPPORTED_REASONING_EFFORT throw from resolveCallWithInfo) into a terminal
|
|
241
|
+
// error finish chunk inside adapterStream — the rejection NEVER reaches our
|
|
242
|
+
// catch. The v0.7.16 throw-based fallback was therefore dead code for the
|
|
243
|
+
// stream path; these tests pin the finish-chunk-based fallback.
|
|
244
|
+
|
|
245
|
+
test("rc.1 stream-level effort rejection (error finish chunk) also triggers the no-effort retry", async () => {
|
|
246
|
+
const store = createStore(":memory:");
|
|
247
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
248
|
+
const dream = createDreamScheduler({ onRun: () => Promise.resolve({ ok: true, skipped: true }) });
|
|
249
|
+
const { memory: a } = service.saveWithDedupe({ type: "project", title: "插件", content: "旧", importance: 3 });
|
|
250
|
+
const { memory: b } = service.saveWithDedupe({ type: "project", title: "插件2", content: "新细节", importance: 4 });
|
|
251
|
+
const calls = [];
|
|
252
|
+
const warnings = [];
|
|
253
|
+
const ctx = {
|
|
254
|
+
logger: { warn: (m) => warnings.push(String(m)) },
|
|
255
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
256
|
+
llm: {
|
|
257
|
+
async *stream(options) {
|
|
258
|
+
calls.push(options);
|
|
259
|
+
if (options.reasoningEffort) {
|
|
260
|
+
yield {
|
|
261
|
+
type: "finish",
|
|
262
|
+
reason: {
|
|
263
|
+
kind: "error",
|
|
264
|
+
failure: {
|
|
265
|
+
code: "UNSUPPORTED_REASONING_EFFORT",
|
|
266
|
+
message: 'provider "mock" model "mock-model" does not support reasoning effort "low"'
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
273
|
+
if (userText.startsWith("id=")) {
|
|
274
|
+
yield { type: "text-delta", index: 0, text: JSON.stringify([
|
|
275
|
+
{ action: "merge", ids: [a.id, b.id], keepSource: b.id, title: "合并标题", content: "合并内容", importance: 4 }
|
|
276
|
+
]) };
|
|
277
|
+
} else {
|
|
278
|
+
yield { type: "text-delta", index: 0, text: "记忆库总览:用户偏好中文。" };
|
|
279
|
+
}
|
|
280
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "low" });
|
|
285
|
+
assert.equal(result.ok, true, "run survives the stream-level effort rejection");
|
|
286
|
+
assert.ok(result.applied > 0, "consolidation still lands changes");
|
|
287
|
+
assert.equal(calls[0].reasoningEffort, "low", "first attempt forwards the effort");
|
|
288
|
+
assert.equal("reasoningEffort" in calls[1], false, "retry omits the rejected effort field");
|
|
289
|
+
assert.ok(warnings.some((w) => w.includes("rejected via stream")), "the stream-level rejection is logged");
|
|
290
|
+
store.close();
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("non-effort stream failures are not retried and the finish-chunk cause reaches the audit row", async () => {
|
|
294
|
+
const store = createStore(":memory:");
|
|
295
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
296
|
+
service.saveWithDedupe({ type: "project", title: "主题", content: "内容" });
|
|
297
|
+
const calls = [];
|
|
298
|
+
const ctx = {
|
|
299
|
+
logger: { warn: () => {} },
|
|
300
|
+
agentDefaultModel: { currentSelection: () => ({ provider: "mock", model: "mock-model" }) },
|
|
301
|
+
llm: {
|
|
302
|
+
async *stream(options) {
|
|
303
|
+
calls.push(options);
|
|
304
|
+
yield { type: "finish", reason: { kind: "error", failure: { code: "PROVIDER_GONE", message: "provider mock is not registered" } } };
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
const dream = createDreamScheduler({ thresholdCount: 1, thresholdChars: 0, delayMs: 0 });
|
|
309
|
+
const result = await dream.runDream(ctx, service, { dreamReasoningEffort: "low" });
|
|
310
|
+
assert.equal(result.ok, false);
|
|
311
|
+
assert.equal(result.error, "llm failed", "the run error stays the stable short string");
|
|
312
|
+
assert.equal(calls.length, 1, "no blind retry when the stream failure is not an effort rejection");
|
|
313
|
+
const row = service.listLlmAudits().find((r) => r.operation_type === "dream_consolidate");
|
|
314
|
+
assert.ok(row && row.status === "error", "failed consolidation still audited");
|
|
315
|
+
assert.ok(
|
|
316
|
+
String(row.error_message).includes("PROVIDER_GONE") && String(row.error_message).includes("provider mock is not registered"),
|
|
317
|
+
"audit error_message carries the finish-chunk cause"
|
|
318
|
+
);
|
|
319
|
+
store.close();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("sleep passes the stream failure accessor so a stream-level effort rejection retries", async () => {
|
|
323
|
+
const { store, service, vectorIndex } = sleepSetup();
|
|
324
|
+
const a = service.saveWithDedupe({ type: "project", title: "主题X", content: "内容A 关于主题X", importance: 3 }).memory;
|
|
325
|
+
const b = service.saveWithDedupe({ type: "project", title: "主题X副本", content: "内容B 关于主题X", importance: 3 }).memory;
|
|
326
|
+
vectorIndex.saveEmbedding(a.id, [1, 0, 0]);
|
|
327
|
+
vectorIndex.saveEmbedding(b.id, [1, 0, 0]);
|
|
328
|
+
const captured = [];
|
|
329
|
+
const ctx = sleepCtx(null, { provider: "mock", model: "sleep-model" }, captured);
|
|
330
|
+
ctx.llm.stream = async function* (options) {
|
|
331
|
+
captured.push(options);
|
|
332
|
+
if (options.reasoningEffort) {
|
|
333
|
+
yield {
|
|
334
|
+
type: "finish",
|
|
335
|
+
reason: { kind: "error", failure: { code: "UNSUPPORTED_REASONING_EFFORT", message: 'provider "mock" model "sleep-model" does not support reasoning effort "low"' } }
|
|
336
|
+
};
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const userText = options.messages.find((m) => m.role === "user")?.content?.[0]?.text ?? "";
|
|
340
|
+
yield { type: "text-delta", index: 0, text: userText.startsWith("候选冲突")
|
|
341
|
+
? JSON.stringify([{ action: "conflict", winner: a.id, loser: b.id, reason: "重复覆盖" }])
|
|
342
|
+
: "[]" };
|
|
343
|
+
yield { type: "finish", reason: { kind: "stop" } };
|
|
344
|
+
};
|
|
345
|
+
const result = await runSleep(ctx, service, baseConfig({ sleepReasoningEffort: "low" }), ctx.logger, { embedder, vectorIndex }, null);
|
|
346
|
+
assert.equal(result.status, "ok", "sleep survives the stream-level effort rejection");
|
|
347
|
+
assert.equal(captured[0].reasoningEffort, "low", "first conflict attempt forwards the effort");
|
|
348
|
+
assert.equal("reasoningEffort" in captured[1], false, "conflict retry omits the rejected effort field");
|
|
349
|
+
store.close();
|
|
350
|
+
});
|