Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-3E63DD?style=flat-square" alt="license"></a>
<a href="https://github.com/slow-stack/mneme/actions"><img src="https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml?style=flat-square&label=CI" alt="CI"></a>
<a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-22%2B-3E63DD?style=flat-square&logo=nodedotjs&logoColor=white" alt="node"></a>
<a href="https://github.com/slow-stack/mneme"><img src="https://img.shields.io/badge/tests-1613%20passed-3E63DD?style=flat-square" alt="tests"></a>
<a href="https://github.com/slow-stack/mneme"><img src="https://img.shields.io/badge/tests-1615%20passed-3E63DD?style=flat-square" alt="tests"></a>
<a href="https://codecov.io/gh/slow-stack/mneme"><img src="https://img.shields.io/codecov/c/github/slow-stack/mneme/main?style=flat-square" alt="coverage"></a>
<a href="https://github.com/awesome-dsh-plugin/awesome-dsh-plugin"><img src="https://awesome-dsh-plugin.com/badge.svg" alt="Awesome"></a>
</p>
Expand Down Expand Up @@ -207,7 +207,7 @@ dsh-mneme-serve # 默认 ~/.dsh/memory + 127.0.0.1:8790

```bash
cd dsh-mneme && npm install
npm test # 1613 个测试
npm test # 1615 个测试
npm run stress # 三轴线压测
npm run sync # src → lib 同步
```
Expand Down Expand Up @@ -414,7 +414,7 @@ Community projects that wire mneme into more platforms — not part of this repo

```bash
cd dsh-mneme && npm install
npm test # 1613 tests
npm test # 1615 tests
npm run stress # three-axis stress test
npm run sync # src → lib sync
```
Expand Down
6 changes: 3 additions & 3 deletions dsh-mneme/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![npm version](https://img.shields.io/npm/v/@modusensus/dsh-mneme?color=blue&label=npm)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
[![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
[![Awesome](https://awesome-dsh-plugin.com/badge.svg)](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
[![tests](https://img.shields.io/badge/tests-1613%20passed-success)](https://github.com/slow-stack/mneme)
[![tests](https://img.shields.io/badge/tests-1615%20passed-success)](https://github.com/slow-stack/mneme)
[![CI](https://img.shields.io/github/actions/workflow/status/slow-stack/mneme/ci.yml)](https://github.com/slow-stack/mneme/actions)
[![node](https://img.shields.io/badge/node-22%2B-blue)](https://nodejs.org)
[![npm downloads](https://img.shields.io/npm/d18m/@modusensus/dsh-mneme.svg?color=blue&label=downloads)](https://www.npmjs.com/package/@modusensus/dsh-mneme)
Expand Down Expand Up @@ -613,7 +613,7 @@ src/
├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列)
└── index.js # 插件接线
lib/ # src 的同步分发产物(npm run sync;发布前由 root prepack 的 check-sync.js 校验一致性;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖)
test/ # 1613 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
test/ # 1615 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步 · check-sync.js 发布闸门 · benchmark-recall.js / benchmark-embed.js / benchmark-rerank.js 基准 · sync-test-badge.mjs 测试徽章 · build-runtime-manifest.mjs 运行时清单
```

Expand All @@ -622,7 +622,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
```bash
cd dsh-mneme
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
npm test # 运行 1613 个测试
npm test # 运行 1615 个测试
npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
```
Expand Down
43 changes: 26 additions & 17 deletions dsh-mneme/lib/summarize.js
Original file line number Diff line number Diff line change
Expand Up @@ -574,23 +574,28 @@ export function createSummarizer(ctx, service, config, deps = {}) {
// 判定纯规则(字符数),无模型参与;被挡下的窗口照常消费游标,否则每个
// turn/end 都会重新评估同一段短文本。skip 原因进审计,作为后续按真实负载
// 决定下一级规则的依据。
//
// 字符数改为无条件计算并写进审计 metadata:它是「下一级预判该不该开、阈值
// 该给多少」的唯一依据,而原先开关一关就一个数都不留(维护者本机 1240 条
// summarize_compress 成功行的 metadata 里窗口字符数为 0 条),验收 4「默认配置
// 下调用量可量化下降」因此无法对账。本改动只加计量,不动任何跳过决策——
// summarizeMinWindowChars 仍为 0 时行为逐字节不变。
const distillChars = messages.reduce(
(total, message) => total + (Array.isArray(message.content) ? message.content : [])
.reduce((n, part) => n + (typeof part?.text === "string" ? part.text.length : 0), 0),
0
);
const minWindowChars = config.summarizeMinWindowChars ?? 0;
if (minWindowChars > 0) {
const distillChars = messages.reduce(
(total, message) => total + (Array.isArray(message.content) ? message.content : [])
.reduce((n, part) => n + (typeof part?.text === "string" ? part.text.length : 0), 0),
0
);
if (distillChars < minWindowChars) {
commitCursor(session.id, nextSeq);
writeAudit({
timestamp: new Date().toISOString(),
model_id: route ? `${route.provider}:${route.model}` : "unknown",
status: "skipped",
error_message: "window-too-small"
});
return;
}
if (minWindowChars > 0 && distillChars < minWindowChars) {
commitCursor(session.id, nextSeq);
writeAudit({
timestamp: new Date().toISOString(),
model_id: route ? `${route.provider}:${route.model}` : "unknown",
status: "skipped",
error_message: "window-too-small",
metadata: { distillChars, minWindowChars }
});
return;
}
// 只有真正准备发起一次 LLM 蒸馏时才占用最小间隔与 run 预算;失败路径仍按
// 原语义占用(调用确实发生过),aborted 则由 finally 回滚。
Expand Down Expand Up @@ -809,8 +814,12 @@ export function createSummarizer(ctx, service, config, deps = {}) {
// 空数组是合法成功:没有记忆写入,但本次事件窗口仍然应被持久消费。
persistCursor(session.id, nextSeq);
}
if (audit && (capped > 0 || deduped > 0 || parsedResult.salvaged)) {
// Issue #239 定标:窗口字符数无条件随成功行落进 metadata。原先只有
// capped / deduped / salvaged 其中一项发生时才建 metadata,默认档位下
// 一条都不留——下一级预判规则的阈值就无从定标。
if (audit) {
audit.metadata = {
distillChars,
parsed: parsed.length,
...(parsedResult.salvaged ? { json_salvaged: true } : {}),
...(capped > 0 ? { capped } : {}),
Expand Down
43 changes: 26 additions & 17 deletions dsh-mneme/src/summarize.js
Original file line number Diff line number Diff line change
Expand Up @@ -574,23 +574,28 @@ export function createSummarizer(ctx, service, config, deps = {}) {
// 判定纯规则(字符数),无模型参与;被挡下的窗口照常消费游标,否则每个
// turn/end 都会重新评估同一段短文本。skip 原因进审计,作为后续按真实负载
// 决定下一级规则的依据。
//
// 字符数改为无条件计算并写进审计 metadata:它是「下一级预判该不该开、阈值
// 该给多少」的唯一依据,而原先开关一关就一个数都不留(维护者本机 1240 条
// summarize_compress 成功行的 metadata 里窗口字符数为 0 条),验收 4「默认配置
// 下调用量可量化下降」因此无法对账。本改动只加计量,不动任何跳过决策——
// summarizeMinWindowChars 仍为 0 时行为逐字节不变。
const distillChars = messages.reduce(
(total, message) => total + (Array.isArray(message.content) ? message.content : [])
.reduce((n, part) => n + (typeof part?.text === "string" ? part.text.length : 0), 0),
0
);
const minWindowChars = config.summarizeMinWindowChars ?? 0;
if (minWindowChars > 0) {
const distillChars = messages.reduce(
(total, message) => total + (Array.isArray(message.content) ? message.content : [])
.reduce((n, part) => n + (typeof part?.text === "string" ? part.text.length : 0), 0),
0
);
if (distillChars < minWindowChars) {
commitCursor(session.id, nextSeq);
writeAudit({
timestamp: new Date().toISOString(),
model_id: route ? `${route.provider}:${route.model}` : "unknown",
status: "skipped",
error_message: "window-too-small"
});
return;
}
if (minWindowChars > 0 && distillChars < minWindowChars) {
commitCursor(session.id, nextSeq);
writeAudit({
timestamp: new Date().toISOString(),
model_id: route ? `${route.provider}:${route.model}` : "unknown",
status: "skipped",
error_message: "window-too-small",
metadata: { distillChars, minWindowChars }
});
return;
}
// 只有真正准备发起一次 LLM 蒸馏时才占用最小间隔与 run 预算;失败路径仍按
// 原语义占用(调用确实发生过),aborted 则由 finally 回滚。
Expand Down Expand Up @@ -809,8 +814,12 @@ export function createSummarizer(ctx, service, config, deps = {}) {
// 空数组是合法成功:没有记忆写入,但本次事件窗口仍然应被持久消费。
persistCursor(session.id, nextSeq);
}
if (audit && (capped > 0 || deduped > 0 || parsedResult.salvaged)) {
// Issue #239 定标:窗口字符数无条件随成功行落进 metadata。原先只有
// capped / deduped / salvaged 其中一项发生时才建 metadata,默认档位下
// 一条都不留——下一级预判规则的阈值就无从定标。
if (audit) {
audit.metadata = {
distillChars,
parsed: parsed.length,
...(parsedResult.salvaged ? { json_salvaged: true } : {}),
...(capped > 0 ? { capped } : {}),
Expand Down
37 changes: 37 additions & 0 deletions dsh-mneme/test/summarize.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,43 @@ test("#239 窗口过短:零 LLM 预判拦下调用,留 skip 审计并消费
assert.equal(service.listLlmAudits().length, 1, "游标已消费:同一窗口不重复评估、不重复审计");
});

// #239 定标埋点:窗口字符数必须落进审计,否则下一级预判规则的阈值无从选起。
// 第二条才是本改动的目的——预判开关关着(默认档位)时行为逐字节不变,但从这一刻
// 起成功行开始留数,验收 4「默认配置下调用量可量化下降」才有账可对。
test("#239 计量:window-too-small 的 skip 行带 distillChars 与阈值", async () => {
const { events, service } = setup({ summarizeMinWindowChars: 1000 });
const handler = events.find((e) => e.name === "session/event").fn;
const session = {
id: "s239-metric-skip",
requestHeader: () => ({ config: { provider: "deepseek", model: "deepseek-chat" } }),
events: [userMessage("好", 1), { seq: 2, type: "turn/end" }]
};
await handler(session, { seq: 2, type: "turn/end" });
const row = service.listLlmAudits()[0];
assert.equal(row.error_message, "window-too-small");
// 计的是**真正发给模型的转录文本**长度(含角色前缀:一条 1 字的「好」实测记 4),
// 与闸门比较的口径同一个数,定标才对得上。这里不锁字面量,锁语义:短于阈值才该 skip。
assert.ok(Number.isInteger(row.metadata?.distillChars) && row.metadata.distillChars > 0,
"skip 行必须带窗口字符数");
assert.equal(row.metadata.minWindowChars, 1000, "阈值随行落档,事后能复核是哪条闸门拦的");
assert.ok(row.metadata.distillChars < row.metadata.minWindowChars, "skip 的前提:窗口短于阈值");
});

test("#239 计量:预判开关关闭时成功行仍记录 distillChars", async () => {
const { events, service } = setup();
const handler = events.find((e) => e.name === "session/event").fn;
const session = {
id: "s239-metric-success",
requestHeader: () => ({ config: { provider: "deepseek", model: "deepseek-chat" } }),
events: [userMessage("今天决定把窗口字符数记进审计", 1), { seq: 2, type: "turn/end" }]
};
await handler(session, { seq: 2, type: "turn/end" });
const row = service.listLlmAudits()[0];
assert.equal(row.status, "success");
assert.ok(Number.isInteger(row.metadata?.distillChars) && row.metadata.distillChars > 0,
"默认档位下也要留数,否则验收 4 无从对账");
});

test("#239 窗口达到阈值时预判放行(不误杀)", async () => {
const { events, calls } = setup({ summarizeMinWindowChars: 5 });
const handler = events.find((e) => e.name === "session/event").fn;
Expand Down
Loading