@modusensus/dsh-mneme 0.8.14 → 0.8.16
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/LICENSE +21 -21
- package/README.md +660 -654
- package/assets/icon.svg +3 -3
- package/bin/cli.mjs +691 -689
- package/bin/dsh-mneme-mcp.mjs +510 -510
- package/bin/dsh-mneme-serve.mjs +151 -151
- package/cordis.patch.yml +21 -21
- package/docs/CONFIGURATION.md +275 -0
- package/docs/CONTINUITY.md +124 -0
- package/docs/DAEMON.md +46 -0
- package/docs/DISTILL.md +162 -0
- package/docs/ENTITIES.md +245 -0
- package/docs/LOCAL_MODEL.md +222 -0
- package/docs/SEMANTIC.md +256 -0
- package/docs/SLEEP.md +164 -0
- package/docs/STORAGE.md +98 -0
- package/lib/api-standalone.js +656 -673
- package/lib/api.js +1605 -1433
- package/lib/bootstrap.js +211 -211
- package/lib/client.js +6354 -5296
- package/lib/commands.js +95 -95
- package/lib/config.js +947 -843
- package/lib/content-hash.js +54 -54
- package/lib/continuity.js +220 -220
- package/lib/document-index.js +94 -94
- package/lib/document.js +303 -303
- package/lib/dream/clustering.js +118 -118
- package/lib/dream/decisions.js +694 -674
- package/lib/dream/narratives.js +44 -44
- package/lib/dream/sleep.js +819 -819
- package/lib/dream.js +1503 -1501
- package/lib/embedding.js +177 -177
- package/lib/entities/extractor.js +252 -252
- package/lib/graph/anchoring.js +179 -179
- package/lib/guide.js +74 -74
- package/lib/heat.js +144 -144
- package/lib/hot-memory.js +108 -46
- package/lib/index.js +552 -503
- package/lib/inject.js +428 -450
- package/lib/lang.js +248 -229
- package/lib/local-embedder.js +358 -358
- package/lib/maintenance.js +162 -142
- package/lib/mirror.js +330 -330
- package/lib/organize.js +450 -450
- package/lib/peak-hours.js +117 -117
- package/lib/placement.js +13 -0
- package/lib/pre-inject-gate.js +340 -0
- package/lib/quality-filter.js +126 -126
- package/lib/recall-stats.js +190 -175
- package/lib/reclaim-http.js +150 -0
- package/lib/reranker.js +269 -269
- package/lib/runtime/adopt.js +176 -176
- package/lib/runtime/closure.js +172 -172
- package/lib/runtime/download.js +247 -247
- package/lib/runtime/layout.js +265 -265
- package/lib/runtime/loader.js +252 -252
- package/lib/runtime/model-fetch.js +361 -361
- package/lib/runtime/provision.js +237 -237
- package/lib/runtime/tarball.js +130 -130
- package/lib/runtime/verify.js +223 -204
- package/lib/safe-slice.js +18 -0
- package/lib/scope.js +146 -146
- package/lib/search/adaptive.js +60 -60
- package/lib/search/bm25.js +96 -96
- package/lib/semantic.js +229 -229
- package/lib/sensitive-scan.js +209 -209
- package/lib/serve.js +215 -215
- package/lib/service.js +2688 -2605
- package/lib/settings.js +528 -511
- package/lib/store.js +3196 -3007
- package/lib/summarize.js +1039 -856
- package/lib/tools.js +743 -737
- package/lib/vector-index.js +110 -110
- package/lib/version-check.js +122 -122
- package/lib/write-admission.js +402 -402
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +118 -118
- package/runtime-manifest.json +526 -526
- package/scripts/benchmark-embed.js +201 -201
- package/scripts/benchmark-recall.js +249 -249
- package/scripts/benchmark-rerank.js +166 -166
- package/scripts/build-runtime-manifest.mjs +224 -224
- package/scripts/changelog-prep.mjs +36 -36
- package/scripts/check-sync.js +42 -42
- package/scripts/e2e-dsh.js +220 -220
- package/scripts/mneme-runtime.mjs +277 -277
- package/scripts/stress-dsh.js +255 -255
- package/scripts/sync-lib.js +47 -47
- package/scripts/sync-test-badge.mjs +69 -69
- package/scripts/test-count-sync.mjs +44 -44
- package/src/api-standalone.js +656 -673
- package/src/api.js +1605 -1433
- package/src/bootstrap.js +211 -211
- package/src/commands.js +95 -95
- package/src/config.js +947 -843
- package/src/content-hash.js +54 -54
- package/src/continuity.js +220 -220
- package/src/document-index.js +94 -94
- package/src/document.js +303 -303
- package/src/dream/clustering.js +118 -118
- package/src/dream/decisions.js +694 -674
- package/src/dream/narratives.js +44 -44
- package/src/dream/sleep.js +819 -819
- package/src/dream.js +1503 -1501
- package/src/embedding.js +177 -177
- package/src/entities/extractor.js +252 -252
- package/src/graph/anchoring.js +179 -179
- package/src/guide.js +74 -74
- package/src/heat.js +144 -144
- package/src/hot-memory.js +108 -46
- package/src/index.js +552 -503
- package/src/inject.js +428 -450
- package/src/lang.js +248 -229
- package/src/local-embedder.js +358 -358
- package/src/maintenance.js +162 -142
- package/src/mirror.js +330 -330
- package/src/organize.js +450 -450
- package/src/peak-hours.js +117 -117
- package/src/placement.js +13 -0
- package/src/pre-inject-gate.js +340 -0
- package/src/quality-filter.js +126 -126
- package/src/recall-stats.js +190 -175
- package/src/reclaim-http.js +150 -0
- package/src/reranker.js +269 -269
- package/src/runtime/adopt.js +176 -176
- package/src/runtime/closure.js +172 -172
- package/src/runtime/download.js +247 -247
- package/src/runtime/layout.js +265 -265
- package/src/runtime/loader.js +252 -252
- package/src/runtime/model-fetch.js +361 -361
- package/src/runtime/provision.js +237 -237
- package/src/runtime/tarball.js +130 -130
- package/src/runtime/verify.js +223 -204
- package/src/safe-slice.js +18 -0
- package/src/scope.js +146 -146
- package/src/search/adaptive.js +60 -60
- package/src/search/bm25.js +96 -96
- package/src/semantic.js +229 -229
- package/src/sensitive-scan.js +209 -209
- package/src/serve.js +215 -215
- package/src/service.js +2688 -2605
- package/src/settings.js +528 -511
- package/src/store.js +3196 -3007
- package/src/summarize.js +1039 -856
- package/src/tools.js +743 -737
- package/src/vector-index.js +110 -110
- package/src/version-check.js +122 -122
- package/src/write-admission.js +402 -402
- package/test/adaptive-inject-budget.test.js +85 -85
- package/test/api-routes-gaps.test.js +899 -218
- package/test/api.test.js +1725 -1690
- package/test/audit.test.js +448 -448
- package/test/baseline-persist.test.js +248 -248
- package/test/benchmark.test.js +94 -94
- package/test/bootstrap.test.js +220 -220
- package/test/changelog-prep.test.js +60 -60
- package/test/client.test.js +1582 -1169
- package/test/clustering.test.js +100 -100
- package/test/commands.test.js +106 -106
- package/test/config.test.js +66 -66
- package/test/conflict-freeze.test.js +300 -300
- package/test/content-hash.test.js +204 -204
- package/test/continuity.test.js +333 -333
- package/test/contrast.test.js +314 -0
- package/test/document-dir.test.js +233 -233
- package/test/document.test.js +671 -671
- package/test/dream-archive-guardrail.test.js +116 -116
- package/test/dream-candidate-hybrid.test.js +281 -281
- package/test/dream-coverage-degrade.test.js +86 -86
- package/test/dream-failure-backoff.test.js +173 -173
- package/test/dream-merge-guardrail.test.js +219 -0
- package/test/dream-peak-hours.test.js +194 -194
- package/test/dream.test.js +1385 -1383
- package/test/embedding-ready.test.js +196 -196
- package/test/entities.test.js +627 -547
- package/test/entity-recall.test.js +179 -179
- package/test/epistemic.test.js +325 -325
- package/test/fnew-0112.test.js +311 -311
- package/test/fnew-03.test.js +422 -422
- package/test/graph-anchoring.test.js +184 -184
- package/test/graph-api.test.js +175 -175
- package/test/graph-inject-budget.test.js +103 -103
- package/test/graph-passive-confirm.test.js +91 -91
- package/test/graph-weight-evolution.test.js +104 -104
- package/test/heat.test.js +175 -175
- package/test/helpers/dream-mock.js +82 -82
- package/test/helpers/peer-worker.mjs +25 -25
- package/test/helpers/runtime-source.js +60 -60
- package/test/helpers/tar-builder.js +80 -80
- package/test/helpers/write-admission-samples.js +308 -308
- package/test/hot-memory.test.js +211 -145
- package/test/inject-content-cap.test.js +91 -91
- package/test/inject-first-round-bm25.test.js +89 -89
- package/test/inject-heat.test.js +77 -77
- package/test/inject-parent-gate.test.js +106 -106
- package/test/inject-pools.test.js +194 -194
- package/test/inject-recall.test.js +144 -105
- package/test/inject-rotation.test.js +161 -161
- package/test/inject.test.js +443 -420
- package/test/lang.test.js +87 -87
- package/test/lastrunat-persist.test.js +101 -101
- package/test/lib-smoke.test.js +96 -96
- package/test/llm-audit.test.js +696 -598
- package/test/llm-message-source.test.js +207 -207
- package/test/local-embedder.test.js +292 -292
- package/test/maintenance.test.js +501 -391
- package/test/mcp-stdio.test.js +385 -385
- package/test/mirror-dirty.test.js +424 -424
- package/test/mirror-edit-digest.test.js +187 -187
- package/test/mirror-generation.test.js +499 -499
- package/test/mirror-types.test.js +208 -208
- package/test/mirror.test.js +249 -249
- package/test/narrative-bars.test.js +162 -162
- package/test/organize.test.js +418 -418
- package/test/peer-blockers.test.js +193 -193
- package/test/placement.test.js +36 -0
- package/test/policy-epoch.test.js +259 -259
- package/test/pre-inject-gate.test.js +444 -0
- package/test/quality-filter.test.js +186 -186
- package/test/reasoning-effort.test.js +752 -752
- package/test/recall-anti-update.test.js +82 -82
- package/test/recall-evals.test.js +253 -253
- package/test/recall-layer.test.js +326 -326
- package/test/recall-stats.test.js +274 -189
- package/test/receipt-chain.test.js +451 -451
- package/test/reclaim-http.test.js +209 -0
- package/test/reflection.test.js +226 -226
- package/test/reindex-backfill.test.js +62 -62
- package/test/reranker-single-logit.test.js +73 -73
- package/test/reranker.test.js +197 -197
- package/test/resident-status.test.js +83 -83
- package/test/runtime-adopt.test.js +206 -206
- package/test/runtime-cli.test.js +89 -89
- package/test/runtime-closure.test.js +193 -193
- package/test/runtime-download.test.js +302 -302
- package/test/runtime-layout.test.js +163 -163
- package/test/runtime-loader.test.js +255 -255
- package/test/runtime-manifest.test.js +159 -159
- package/test/runtime-model-fetch.test.js +348 -348
- package/test/runtime-provision.test.js +101 -101
- package/test/runtime-tarball.test.js +81 -81
- package/test/runtime-verify.test.js +319 -279
- package/test/scope-candidates.test.js +239 -239
- package/test/scope-explicit.test.js +452 -452
- package/test/scope-inject-soft.test.js +106 -106
- package/test/scope-retrieval.test.js +197 -197
- package/test/scope-storage.test.js +255 -255
- package/test/scope-strict.test.js +367 -367
- package/test/search-fusion.test.js +90 -90
- package/test/semantic.test.js +124 -124
- package/test/sensitive-scan.test.js +207 -207
- package/test/serve-bin.test.js +121 -121
- package/test/serve.test.js +196 -196
- package/test/service-search.test.js +174 -174
- package/test/service.test.js +410 -405
- package/test/settings.test.js +269 -261
- package/test/skip-invalid-restore.test.js +262 -262
- package/test/sleep-action-set.test.js +422 -422
- package/test/sleep-heat.test.js +126 -126
- package/test/sleep-scheduler-rearm.test.js +89 -89
- package/test/sleep.test.js +527 -527
- package/test/standalone-api.test.js +751 -751
- package/test/store-vector-cache.test.js +98 -98
- package/test/store.test.js +504 -396
- package/test/stress.test.js +209 -209
- package/test/summarize-occurred-at.test.js +127 -0
- package/test/summarize-reasoning-effort.test.js +189 -189
- package/test/summarize.test.js +1639 -1465
- package/test/supersede-state-tracking.test.js +394 -0
- package/test/test-count-sync.test.js +60 -60
- package/test/tools-dto-schema-matrix.test.js +325 -325
- package/test/tools-memory-get-schema.test.js +65 -65
- package/test/tools.test.js +428 -374
- package/test/updated-at-semantics.test.js +113 -113
- package/test/vector-index.test.js +221 -221
- package/test/version-check.test.js +152 -152
- package/test/webServer-optional.test.js +82 -82
- package/test/write-admission.test.js +668 -668
|
@@ -1,189 +1,189 @@
|
|
|
1
|
-
// Regression for issue #315: the distill (summarize) LLM call gets a reasoning
|
|
2
|
-
// effort knob, mirroring the entity-extraction shape (issue #109) and reusing
|
|
3
|
-
// the dream/sleep fallback machinery. Pins:
|
|
4
|
-
// - explicit effort is forwarded on the distill stream call;
|
|
5
|
-
// - default 'none' omits the field entirely (current behavior unchanged);
|
|
6
|
-
// - a stream-level effort rejection (error finish chunk, the rc.1 shape that
|
|
7
|
-
// never reaches catch) is retried once WITHOUT the field;
|
|
8
|
-
// - non-effort stream failures are NOT retried (no blind double calls) and
|
|
9
|
-
// the aborted window stays retryable (cursor not consumed);
|
|
10
|
-
// - 'off' works like any explicit effort (rejected → retried without it).
|
|
11
|
-
// Mock shapes (ctx.on subscription capture, {delta} text chunks, finish
|
|
12
|
-
// {reason:{kind,failure}}) follow the existing summarize.test.js conventions.
|
|
13
|
-
import test from "node:test";
|
|
14
|
-
import assert from "node:assert/strict";
|
|
15
|
-
import { createSummarizer } from "../src/summarize.js";
|
|
16
|
-
import { createStore } from "../src/store.js";
|
|
17
|
-
import { createService } from "../src/service.js";
|
|
18
|
-
|
|
19
|
-
const ENTRIES_JSON = JSON.stringify([
|
|
20
|
-
{ type: "decision", title: "选型", content: "确定用 node:sqlite", importance: 4 }
|
|
21
|
-
]);
|
|
22
|
-
|
|
23
|
-
function userMessage(text, seq) {
|
|
24
|
-
return {
|
|
25
|
-
seq,
|
|
26
|
-
type: "user/message",
|
|
27
|
-
data: { source: { kind: "user" }, content: [{ type: "text", text }] }
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function setup(configOver = {}, stream) {
|
|
32
|
-
const store = createStore(":memory:");
|
|
33
|
-
const service = createService({ store, mirror: null, config: {} });
|
|
34
|
-
const calls = [];
|
|
35
|
-
const events = [];
|
|
36
|
-
const ctx = {
|
|
37
|
-
on(name, fn) {
|
|
38
|
-
events.push({ name, fn });
|
|
39
|
-
return () => {};
|
|
40
|
-
},
|
|
41
|
-
llm: {
|
|
42
|
-
stream(options) {
|
|
43
|
-
calls.push(options);
|
|
44
|
-
return stream(options);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
const config = { autoSummarize: true, distillRateLimitIntervalMs: 0, ...configOver };
|
|
49
|
-
createSummarizer(ctx, service, config, {});
|
|
50
|
-
const handler = events.find((e) => e.name === "session/event").fn;
|
|
51
|
-
const session = {
|
|
52
|
-
id: "s315",
|
|
53
|
-
requestHeader: () => ({ config: { provider: "deepseek", model: "deepseek-chat" } }),
|
|
54
|
-
events: [userMessage("帮我选型", 1), { seq: 2, type: "turn/end" }]
|
|
55
|
-
};
|
|
56
|
-
return { store, service, calls, handler, session };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
test("issue#315: explicit summarizeReasoningEffort is forwarded on the distill call", async () => {
|
|
60
|
-
const { store, calls, handler, session } = setup(
|
|
61
|
-
{ summarizeReasoningEffort: "low" },
|
|
62
|
-
() => (async function* () {
|
|
63
|
-
yield { type: "text-delta", delta: ENTRIES_JSON };
|
|
64
|
-
yield { type: "finish", kind: "ok" };
|
|
65
|
-
})()
|
|
66
|
-
);
|
|
67
|
-
await handler(session, { seq: 2, type: "turn/end" });
|
|
68
|
-
assert.equal(calls.length, 1);
|
|
69
|
-
assert.equal(calls[0].reasoningEffort, "low", "the configured effort rides the distill call");
|
|
70
|
-
store.close();
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test("issue#315: default 'none' omits the reasoningEffort field entirely", async () => {
|
|
74
|
-
const { store, calls, handler, session } = setup(
|
|
75
|
-
{},
|
|
76
|
-
() => (async function* () {
|
|
77
|
-
yield { type: "text-delta", delta: ENTRIES_JSON };
|
|
78
|
-
yield { type: "finish", kind: "ok" };
|
|
79
|
-
})()
|
|
80
|
-
);
|
|
81
|
-
await handler(session, { seq: 2, type: "turn/end" });
|
|
82
|
-
assert.equal(calls.length, 1);
|
|
83
|
-
assert.equal("reasoningEffort" in calls[0], false, "unset/'none' must not add the field");
|
|
84
|
-
assert.equal(store.count(), 1, "entries still stored as before");
|
|
85
|
-
store.close();
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
test("issue#315: stream-level effort rejection retries once without the field", async () => {
|
|
89
|
-
const { store, calls, handler, session } = setup(
|
|
90
|
-
{ summarizeReasoningEffort: "low" },
|
|
91
|
-
(options) => (async function* () {
|
|
92
|
-
if (options.reasoningEffort) {
|
|
93
|
-
// rc.1 shape: the provider rejection arrives as a terminal error finish
|
|
94
|
-
// chunk (adapterStream never throws), so the retry must be triggered
|
|
95
|
-
// from the chunk path, not a catch block.
|
|
96
|
-
yield {
|
|
97
|
-
type: "finish",
|
|
98
|
-
reason: {
|
|
99
|
-
kind: "error",
|
|
100
|
-
failure: {
|
|
101
|
-
code: "UNSUPPORTED_REASONING_EFFORT",
|
|
102
|
-
message: 'provider "deepseek" model "deepseek-chat" does not support reasoning effort "low"'
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
108
|
-
yield { type: "text-delta", delta: ENTRIES_JSON };
|
|
109
|
-
yield { type: "finish", kind: "ok" };
|
|
110
|
-
})()
|
|
111
|
-
);
|
|
112
|
-
await handler(session, { seq: 2, type: "turn/end" });
|
|
113
|
-
assert.equal(calls.length, 2, "exactly one no-effort retry");
|
|
114
|
-
assert.equal(calls[0].reasoningEffort, "low", "first attempt forwards the effort");
|
|
115
|
-
assert.equal("reasoningEffort" in calls[1], false, "retry omits the rejected effort field");
|
|
116
|
-
assert.equal(store.count(), 1, "the retried run still stores its entry");
|
|
117
|
-
store.close();
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
test("issue#315: non-effort stream failures are not retried and stay abortable", async () => {
|
|
121
|
-
const { store, calls, handler, session } = setup(
|
|
122
|
-
{ summarizeReasoningEffort: "low" },
|
|
123
|
-
() => (async function* () {
|
|
124
|
-
yield { type: "finish", reason: { kind: "error", failure: { code: "PROVIDER_GONE", message: "provider deepseek is not registered" } } };
|
|
125
|
-
})()
|
|
126
|
-
);
|
|
127
|
-
await handler(session, { seq: 2, type: "turn/end" });
|
|
128
|
-
assert.equal(calls.length, 1, "no blind retry when the stream failure is not an effort rejection");
|
|
129
|
-
assert.equal(store.count(), 0, "failed window stores nothing");
|
|
130
|
-
store.close();
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("issue#315: off works like any explicit effort (rejected → retried without it)", async () => {
|
|
134
|
-
const { store, calls, handler, session } = setup(
|
|
135
|
-
{ summarizeReasoningEffort: "off" },
|
|
136
|
-
(options) => (async function* () {
|
|
137
|
-
if (options.reasoningEffort) {
|
|
138
|
-
yield { type: "finish", reason: { kind: "error", failure: { code: "UNSUPPORTED_REASONING_EFFORT", message: 'reasoning effort "off" is not supported here' } } };
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
yield { type: "text-delta", delta: ENTRIES_JSON };
|
|
142
|
-
yield { type: "finish", kind: "ok" };
|
|
143
|
-
})()
|
|
144
|
-
);
|
|
145
|
-
await handler(session, { seq: 2, type: "turn/end" });
|
|
146
|
-
assert.equal(calls.length, 2);
|
|
147
|
-
assert.equal(calls[0].reasoningEffort, "off");
|
|
148
|
-
assert.equal(store.count(), 1);
|
|
149
|
-
store.close();
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
test("issue#315: successful effort-fallback retry records audit as success, not the first failure", async () => {
|
|
153
|
-
const { store, service, calls, handler, session } = setup(
|
|
154
|
-
{ summarizeReasoningEffort: "low" },
|
|
155
|
-
(options) => (async function* () {
|
|
156
|
-
if (options.reasoningEffort) {
|
|
157
|
-
// first attempt is rejected (effort unsupported)…
|
|
158
|
-
yield { type: "finish", reason: { kind: "error", failure: { code: "UNSUPPORTED_REASONING_EFFORT", message: 'reasoning effort "low" rejected' } } };
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
// …but the no-effort retry succeeds.
|
|
162
|
-
yield { type: "text-delta", delta: ENTRIES_JSON };
|
|
163
|
-
yield { type: "finish", kind: "ok" };
|
|
164
|
-
})()
|
|
165
|
-
);
|
|
166
|
-
await handler(session, { seq: 2, type: "turn/end" });
|
|
167
|
-
assert.equal(calls.length, 2, "effort rejection + one retry");
|
|
168
|
-
const rows = service.listLlmAudits();
|
|
169
|
-
const summarize = rows.find((r) => r.operation_type === "summarize_compress");
|
|
170
|
-
assert.ok(summarize, "summarize audit row present");
|
|
171
|
-
assert.equal(summarize.status, "success",
|
|
172
|
-
"the retried run must record success, not the first attempt's error status");
|
|
173
|
-
store.close();
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
test("issue#315: abort is not misread as an effort rejection even when the message matches the pattern", async () => {
|
|
177
|
-
const { store, calls, handler, session } = setup(
|
|
178
|
-
{ summarizeReasoningEffort: "low" },
|
|
179
|
-
() => (async function* () {
|
|
180
|
-
// The message matches EFFORT_REJECT_RE on purpose: without the AbortError
|
|
181
|
-
// guard in withEffortFallback this would be misread as an effort rejection
|
|
182
|
-
// and trigger a wasteful no-effort retry.
|
|
183
|
-
throw Object.assign(new Error("aborted by dispose: UNSUPPORTED_REASONING_EFFORT"), { name: "AbortError" });
|
|
184
|
-
})()
|
|
185
|
-
);
|
|
186
|
-
await handler(session, { seq: 2, type: "turn/end" });
|
|
187
|
-
assert.equal(calls.length, 1, "abort must not trigger a no-effort fallback retry");
|
|
188
|
-
store.close();
|
|
189
|
-
});
|
|
1
|
+
// Regression for issue #315: the distill (summarize) LLM call gets a reasoning
|
|
2
|
+
// effort knob, mirroring the entity-extraction shape (issue #109) and reusing
|
|
3
|
+
// the dream/sleep fallback machinery. Pins:
|
|
4
|
+
// - explicit effort is forwarded on the distill stream call;
|
|
5
|
+
// - default 'none' omits the field entirely (current behavior unchanged);
|
|
6
|
+
// - a stream-level effort rejection (error finish chunk, the rc.1 shape that
|
|
7
|
+
// never reaches catch) is retried once WITHOUT the field;
|
|
8
|
+
// - non-effort stream failures are NOT retried (no blind double calls) and
|
|
9
|
+
// the aborted window stays retryable (cursor not consumed);
|
|
10
|
+
// - 'off' works like any explicit effort (rejected → retried without it).
|
|
11
|
+
// Mock shapes (ctx.on subscription capture, {delta} text chunks, finish
|
|
12
|
+
// {reason:{kind,failure}}) follow the existing summarize.test.js conventions.
|
|
13
|
+
import test from "node:test";
|
|
14
|
+
import assert from "node:assert/strict";
|
|
15
|
+
import { createSummarizer } from "../src/summarize.js";
|
|
16
|
+
import { createStore } from "../src/store.js";
|
|
17
|
+
import { createService } from "../src/service.js";
|
|
18
|
+
|
|
19
|
+
const ENTRIES_JSON = JSON.stringify([
|
|
20
|
+
{ type: "decision", title: "选型", content: "确定用 node:sqlite", importance: 4 }
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function userMessage(text, seq) {
|
|
24
|
+
return {
|
|
25
|
+
seq,
|
|
26
|
+
type: "user/message",
|
|
27
|
+
data: { source: { kind: "user" }, content: [{ type: "text", text }] }
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function setup(configOver = {}, stream) {
|
|
32
|
+
const store = createStore(":memory:");
|
|
33
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
34
|
+
const calls = [];
|
|
35
|
+
const events = [];
|
|
36
|
+
const ctx = {
|
|
37
|
+
on(name, fn) {
|
|
38
|
+
events.push({ name, fn });
|
|
39
|
+
return () => {};
|
|
40
|
+
},
|
|
41
|
+
llm: {
|
|
42
|
+
stream(options) {
|
|
43
|
+
calls.push(options);
|
|
44
|
+
return stream(options);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
const config = { autoSummarize: true, distillRateLimitIntervalMs: 0, ...configOver };
|
|
49
|
+
createSummarizer(ctx, service, config, {});
|
|
50
|
+
const handler = events.find((e) => e.name === "session/event").fn;
|
|
51
|
+
const session = {
|
|
52
|
+
id: "s315",
|
|
53
|
+
requestHeader: () => ({ config: { provider: "deepseek", model: "deepseek-chat" } }),
|
|
54
|
+
events: [userMessage("帮我选型", 1), { seq: 2, type: "turn/end" }]
|
|
55
|
+
};
|
|
56
|
+
return { store, service, calls, handler, session };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
test("issue#315: explicit summarizeReasoningEffort is forwarded on the distill call", async () => {
|
|
60
|
+
const { store, calls, handler, session } = setup(
|
|
61
|
+
{ summarizeReasoningEffort: "low" },
|
|
62
|
+
() => (async function* () {
|
|
63
|
+
yield { type: "text-delta", delta: ENTRIES_JSON };
|
|
64
|
+
yield { type: "finish", kind: "ok" };
|
|
65
|
+
})()
|
|
66
|
+
);
|
|
67
|
+
await handler(session, { seq: 2, type: "turn/end" });
|
|
68
|
+
assert.equal(calls.length, 1);
|
|
69
|
+
assert.equal(calls[0].reasoningEffort, "low", "the configured effort rides the distill call");
|
|
70
|
+
store.close();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("issue#315: default 'none' omits the reasoningEffort field entirely", async () => {
|
|
74
|
+
const { store, calls, handler, session } = setup(
|
|
75
|
+
{},
|
|
76
|
+
() => (async function* () {
|
|
77
|
+
yield { type: "text-delta", delta: ENTRIES_JSON };
|
|
78
|
+
yield { type: "finish", kind: "ok" };
|
|
79
|
+
})()
|
|
80
|
+
);
|
|
81
|
+
await handler(session, { seq: 2, type: "turn/end" });
|
|
82
|
+
assert.equal(calls.length, 1);
|
|
83
|
+
assert.equal("reasoningEffort" in calls[0], false, "unset/'none' must not add the field");
|
|
84
|
+
assert.equal(store.count(), 1, "entries still stored as before");
|
|
85
|
+
store.close();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("issue#315: stream-level effort rejection retries once without the field", async () => {
|
|
89
|
+
const { store, calls, handler, session } = setup(
|
|
90
|
+
{ summarizeReasoningEffort: "low" },
|
|
91
|
+
(options) => (async function* () {
|
|
92
|
+
if (options.reasoningEffort) {
|
|
93
|
+
// rc.1 shape: the provider rejection arrives as a terminal error finish
|
|
94
|
+
// chunk (adapterStream never throws), so the retry must be triggered
|
|
95
|
+
// from the chunk path, not a catch block.
|
|
96
|
+
yield {
|
|
97
|
+
type: "finish",
|
|
98
|
+
reason: {
|
|
99
|
+
kind: "error",
|
|
100
|
+
failure: {
|
|
101
|
+
code: "UNSUPPORTED_REASONING_EFFORT",
|
|
102
|
+
message: 'provider "deepseek" model "deepseek-chat" does not support reasoning effort "low"'
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
yield { type: "text-delta", delta: ENTRIES_JSON };
|
|
109
|
+
yield { type: "finish", kind: "ok" };
|
|
110
|
+
})()
|
|
111
|
+
);
|
|
112
|
+
await handler(session, { seq: 2, type: "turn/end" });
|
|
113
|
+
assert.equal(calls.length, 2, "exactly one no-effort retry");
|
|
114
|
+
assert.equal(calls[0].reasoningEffort, "low", "first attempt forwards the effort");
|
|
115
|
+
assert.equal("reasoningEffort" in calls[1], false, "retry omits the rejected effort field");
|
|
116
|
+
assert.equal(store.count(), 1, "the retried run still stores its entry");
|
|
117
|
+
store.close();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("issue#315: non-effort stream failures are not retried and stay abortable", async () => {
|
|
121
|
+
const { store, calls, handler, session } = setup(
|
|
122
|
+
{ summarizeReasoningEffort: "low" },
|
|
123
|
+
() => (async function* () {
|
|
124
|
+
yield { type: "finish", reason: { kind: "error", failure: { code: "PROVIDER_GONE", message: "provider deepseek is not registered" } } };
|
|
125
|
+
})()
|
|
126
|
+
);
|
|
127
|
+
await handler(session, { seq: 2, type: "turn/end" });
|
|
128
|
+
assert.equal(calls.length, 1, "no blind retry when the stream failure is not an effort rejection");
|
|
129
|
+
assert.equal(store.count(), 0, "failed window stores nothing");
|
|
130
|
+
store.close();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("issue#315: off works like any explicit effort (rejected → retried without it)", async () => {
|
|
134
|
+
const { store, calls, handler, session } = setup(
|
|
135
|
+
{ summarizeReasoningEffort: "off" },
|
|
136
|
+
(options) => (async function* () {
|
|
137
|
+
if (options.reasoningEffort) {
|
|
138
|
+
yield { type: "finish", reason: { kind: "error", failure: { code: "UNSUPPORTED_REASONING_EFFORT", message: 'reasoning effort "off" is not supported here' } } };
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
yield { type: "text-delta", delta: ENTRIES_JSON };
|
|
142
|
+
yield { type: "finish", kind: "ok" };
|
|
143
|
+
})()
|
|
144
|
+
);
|
|
145
|
+
await handler(session, { seq: 2, type: "turn/end" });
|
|
146
|
+
assert.equal(calls.length, 2);
|
|
147
|
+
assert.equal(calls[0].reasoningEffort, "off");
|
|
148
|
+
assert.equal(store.count(), 1);
|
|
149
|
+
store.close();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("issue#315: successful effort-fallback retry records audit as success, not the first failure", async () => {
|
|
153
|
+
const { store, service, calls, handler, session } = setup(
|
|
154
|
+
{ summarizeReasoningEffort: "low" },
|
|
155
|
+
(options) => (async function* () {
|
|
156
|
+
if (options.reasoningEffort) {
|
|
157
|
+
// first attempt is rejected (effort unsupported)…
|
|
158
|
+
yield { type: "finish", reason: { kind: "error", failure: { code: "UNSUPPORTED_REASONING_EFFORT", message: 'reasoning effort "low" rejected' } } };
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// …but the no-effort retry succeeds.
|
|
162
|
+
yield { type: "text-delta", delta: ENTRIES_JSON };
|
|
163
|
+
yield { type: "finish", kind: "ok" };
|
|
164
|
+
})()
|
|
165
|
+
);
|
|
166
|
+
await handler(session, { seq: 2, type: "turn/end" });
|
|
167
|
+
assert.equal(calls.length, 2, "effort rejection + one retry");
|
|
168
|
+
const rows = service.listLlmAudits();
|
|
169
|
+
const summarize = rows.find((r) => r.operation_type === "summarize_compress");
|
|
170
|
+
assert.ok(summarize, "summarize audit row present");
|
|
171
|
+
assert.equal(summarize.status, "success",
|
|
172
|
+
"the retried run must record success, not the first attempt's error status");
|
|
173
|
+
store.close();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("issue#315: abort is not misread as an effort rejection even when the message matches the pattern", async () => {
|
|
177
|
+
const { store, calls, handler, session } = setup(
|
|
178
|
+
{ summarizeReasoningEffort: "low" },
|
|
179
|
+
() => (async function* () {
|
|
180
|
+
// The message matches EFFORT_REJECT_RE on purpose: without the AbortError
|
|
181
|
+
// guard in withEffortFallback this would be misread as an effort rejection
|
|
182
|
+
// and trigger a wasteful no-effort retry.
|
|
183
|
+
throw Object.assign(new Error("aborted by dispose: UNSUPPORTED_REASONING_EFFORT"), { name: "AbortError" });
|
|
184
|
+
})()
|
|
185
|
+
);
|
|
186
|
+
await handler(session, { seq: 2, type: "turn/end" });
|
|
187
|
+
assert.equal(calls.length, 1, "abort must not trigger a no-effort fallback retry");
|
|
188
|
+
store.close();
|
|
189
|
+
});
|