pi-mega-compact 0.8.21 → 0.8.22
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/dist/extensions/dashboard-server/dashboard-client-core.js +201 -0
- package/dist/extensions/dashboard-server/dashboard-client-game.js +241 -0
- package/dist/extensions/dashboard-server/dashboard-client-repos.js +212 -0
- package/dist/extensions/dashboard-server/dashboard-client.js +19 -0
- package/dist/extensions/dashboard-server/html.js +2 -621
- package/dist/extensions/dashboard-server/routes-core.js +62 -0
- package/dist/extensions/dashboard-server/routes-game.js +323 -0
- package/dist/extensions/dashboard-server/routes-repo.js +170 -0
- package/dist/extensions/dashboard-server/routes-sessions.js +159 -0
- package/dist/extensions/dashboard-server/routes.js +10 -0
- package/dist/extensions/dashboard-server/server.js +26 -623
- package/dist/extensions/mega-commands.js +4 -3
- package/dist/extensions/mega-events/agent-handlers.js +2 -1
- package/dist/extensions/mega-events/compact-handlers.js +26 -0
- package/dist/extensions/mega-events/session-handlers.js +2 -1
- package/dist/extensions/mega-pipeline/compact.js +3 -2
- package/dist/extensions/mega-runtime/state.js +7 -7
- package/dist/src/dedup/raptor/promote.test.js +5 -5
- package/dist/src/dedup/sprint12.test.js +7 -7
- package/dist/src/dedup-engine.test.js +29 -29
- package/dist/src/e2e.test.js +38 -38
- package/dist/src/engine.js +3 -3
- package/dist/src/engine.test.js +6 -6
- package/dist/src/importance.js +197 -0
- package/dist/src/importance.test.js +372 -0
- package/dist/src/ratio.bench.test.js +18 -18
- package/dist/src/recall.js +6 -5
- package/dist/src/recall.test.js +85 -27
- package/dist/src/sprint14.test.js +2 -2
- package/dist/src/store/migrate.test.js +5 -5
- package/dist/src/store/sprint10.test.js +5 -5
- package/dist/src/store/sqlite/global-index.js +5 -174
- package/dist/src/store/sqlite/global-sessions.js +190 -0
- package/dist/src/vector-read.js +168 -0
- package/dist/src/vector-search.js +191 -0
- package/dist/src/vectorStore.js +10 -297
- package/dist/src/vectorStore.test.js +32 -32
- package/extensions/dashboard-server/dashboard-client-core.ts +202 -0
- package/extensions/dashboard-server/dashboard-client-game.ts +242 -0
- package/extensions/dashboard-server/dashboard-client-repos.ts +213 -0
- package/extensions/dashboard-server/dashboard-client.ts +21 -0
- package/extensions/dashboard-server/html.ts +2 -621
- package/extensions/dashboard-server/routes-core.ts +113 -0
- package/extensions/dashboard-server/routes-game.ts +386 -0
- package/extensions/dashboard-server/routes-repo.ts +212 -0
- package/extensions/dashboard-server/routes-sessions.ts +195 -0
- package/extensions/dashboard-server/routes.ts +13 -0
- package/extensions/dashboard-server/server.ts +37 -700
- package/extensions/mega-commands.ts +4 -3
- package/extensions/mega-events/agent-handlers.ts +2 -1
- package/extensions/mega-events/compact-handlers.ts +28 -0
- package/extensions/mega-events/session-handlers.ts +2 -1
- package/extensions/mega-pipeline/compact.ts +3 -2
- package/extensions/mega-runtime/state.ts +7 -7
- package/extensions/openclaw-mega-compact.ts +2 -2
- package/package.json +1 -1
- package/src/dedup/raptor/promote.test.ts +5 -5
- package/src/dedup/sprint12.test.ts +7 -7
- package/src/dedup-engine.test.ts +30 -30
- package/src/e2e.test.ts +38 -38
- package/src/engine.test.ts +6 -6
- package/src/engine.ts +3 -3
- package/src/importance.test.ts +538 -0
- package/src/importance.ts +312 -0
- package/src/ratio.bench.test.ts +18 -18
- package/src/recall.test.ts +101 -29
- package/src/recall.ts +9 -9
- package/src/sprint14.test.ts +2 -2
- package/src/store/migrate.test.ts +5 -5
- package/src/store/sprint10.test.ts +5 -5
- package/src/store/sqlite/global-index.ts +18 -290
- package/src/store/sqlite/global-sessions.ts +291 -0
- package/src/vector-read.ts +237 -0
- package/src/vector-search.ts +231 -0
- package/src/vectorStore.test.ts +32 -32
- package/src/vectorStore.ts +29 -356
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* importance.test.ts — S40A unit tests for the importance scoring engine.
|
|
3
|
+
*
|
|
4
|
+
* Covers all exports: ContextItemType, DEFAULT_MULTIPLIERS, detectItemType,
|
|
5
|
+
* ageDecay, recencyBoost, retentionBoost, score, preservationCutoff,
|
|
6
|
+
* itemsToPreserve, scoreEngineMessages.
|
|
7
|
+
*
|
|
8
|
+
* Determinism is enforced — same inputs MUST produce same outputs.
|
|
9
|
+
*/
|
|
10
|
+
import { test } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import type { EngineMessage } from "./types.js";
|
|
13
|
+
import {
|
|
14
|
+
ContextItemType,
|
|
15
|
+
DEFAULT_MULTIPLIERS,
|
|
16
|
+
detectItemType,
|
|
17
|
+
ageDecay,
|
|
18
|
+
recencyBoost,
|
|
19
|
+
retentionBoost,
|
|
20
|
+
score,
|
|
21
|
+
preservationCutoff,
|
|
22
|
+
itemsToPreserve,
|
|
23
|
+
scoreEngineMessages,
|
|
24
|
+
} from "./importance.js";
|
|
25
|
+
|
|
26
|
+
const NOW = 1_700_000_000_000; // fixed epoch ms for determinism
|
|
27
|
+
|
|
28
|
+
// ---- S40A-1: types and enums ----
|
|
29
|
+
|
|
30
|
+
test("ContextItemType enum has 8 variants with snake_case values", () => {
|
|
31
|
+
assert.equal(ContextItemType.UserMessage, "user_message");
|
|
32
|
+
assert.equal(ContextItemType.AssistantMessage, "assistant_message");
|
|
33
|
+
assert.equal(ContextItemType.SystemMessage, "system_message");
|
|
34
|
+
assert.equal(ContextItemType.CodeBlock, "code_block");
|
|
35
|
+
assert.equal(ContextItemType.Error, "error");
|
|
36
|
+
assert.equal(ContextItemType.Decision, "decision");
|
|
37
|
+
assert.equal(ContextItemType.FileModification, "file_modification");
|
|
38
|
+
assert.equal(ContextItemType.ToolExecution, "tool_execution");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ---- S40A-2: DEFAULT_MULTIPLIERS ----
|
|
42
|
+
|
|
43
|
+
test("DEFAULT_MULTIPLIERS matches the Rust reference values", () => {
|
|
44
|
+
assert.equal(DEFAULT_MULTIPLIERS[ContextItemType.UserMessage], 1.5);
|
|
45
|
+
assert.equal(DEFAULT_MULTIPLIERS[ContextItemType.AssistantMessage], 1.0);
|
|
46
|
+
assert.equal(DEFAULT_MULTIPLIERS[ContextItemType.SystemMessage], 0.5);
|
|
47
|
+
assert.equal(DEFAULT_MULTIPLIERS[ContextItemType.CodeBlock], 1.2);
|
|
48
|
+
assert.equal(DEFAULT_MULTIPLIERS[ContextItemType.Error], 2.0);
|
|
49
|
+
assert.equal(DEFAULT_MULTIPLIERS[ContextItemType.Decision], 2.5);
|
|
50
|
+
assert.equal(DEFAULT_MULTIPLIERS[ContextItemType.FileModification], 1.8);
|
|
51
|
+
assert.equal(DEFAULT_MULTIPLIERS[ContextItemType.ToolExecution], 1.3);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("DEFAULT_MULTIPLIERS has an entry for every ContextItemType variant", () => {
|
|
55
|
+
for (const t of Object.values(ContextItemType)) {
|
|
56
|
+
assert.equal(
|
|
57
|
+
typeof DEFAULT_MULTIPLIERS[t],
|
|
58
|
+
"number",
|
|
59
|
+
`missing multiplier for ${t}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ---- S40A-3: ageDecay ----
|
|
65
|
+
|
|
66
|
+
test("ageDecay: fresh (age=0) returns 0", () => {
|
|
67
|
+
assert.equal(ageDecay(0), 0);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("ageDecay: 1 hour at 5%/hr returns 0.05", () => {
|
|
71
|
+
assert.equal(ageDecay(3_600_000), 0.05);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("ageDecay: 14 hours caps at 0.7 (maxDecay)", () => {
|
|
75
|
+
assert.equal(ageDecay(14 * 3_600_000), 0.7);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("ageDecay: beyond 14 hours stays capped at 0.7", () => {
|
|
79
|
+
assert.equal(ageDecay(100 * 3_600_000), 0.7);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("ageDecay: negative age (future timestamp) returns 0", () => {
|
|
83
|
+
assert.equal(ageDecay(-5_000), 0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("ageDecay: custom rate and max override defaults", () => {
|
|
87
|
+
// 10h at 0.1/hr = 1.0 → capped at 0.8.
|
|
88
|
+
assert.equal(ageDecay(10 * 3_600_000, 0.1, 0.8), 0.8);
|
|
89
|
+
// 2h at 0.1/hr = 0.2.
|
|
90
|
+
assert.equal(ageDecay(2 * 3_600_000, 0.1, 0.8), 0.2);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// ---- S40A-4: recencyBoost + retentionBoost ----
|
|
94
|
+
|
|
95
|
+
test("recencyBoost: <5min returns 1.2", () => {
|
|
96
|
+
assert.equal(recencyBoost(299_000), 1.2);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("recencyBoost: >5min returns 1.0", () => {
|
|
100
|
+
assert.equal(recencyBoost(301_000), 1.0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("recencyBoost: exactly 5min (300_000ms) returns 1.0 (boundary)", () => {
|
|
104
|
+
assert.equal(recencyBoost(300_000), 1.0);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("recencyBoost: custom threshold respected", () => {
|
|
108
|
+
assert.equal(recencyBoost(5000, 10_000), 1.2);
|
|
109
|
+
assert.equal(recencyBoost(11_000, 10_000), 1.0);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("retentionBoost: flagged returns 3.0", () => {
|
|
113
|
+
assert.equal(retentionBoost(true), 3.0);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("retentionBoost: unflagged returns 1.0", () => {
|
|
117
|
+
assert.equal(retentionBoost(false), 1.0);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// ---- S40A-5: detectItemType ----
|
|
121
|
+
|
|
122
|
+
test("detectItemType: role=tool → ToolExecution (priority 1)", () => {
|
|
123
|
+
// Even if content looks like an error, tool role wins.
|
|
124
|
+
assert.equal(
|
|
125
|
+
detectItemType("error: something failed", "tool"),
|
|
126
|
+
ContextItemType.ToolExecution,
|
|
127
|
+
);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("detectItemType: role=custom → SystemMessage (priority 2)", () => {
|
|
131
|
+
assert.equal(
|
|
132
|
+
detectItemType("anything here", "custom"),
|
|
133
|
+
ContextItemType.SystemMessage,
|
|
134
|
+
);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("detectItemType: error content → Error", () => {
|
|
138
|
+
assert.equal(
|
|
139
|
+
detectItemType("Error: ENOENT at src/config.ts:42", "assistant"),
|
|
140
|
+
ContextItemType.Error,
|
|
141
|
+
);
|
|
142
|
+
assert.equal(
|
|
143
|
+
detectItemType("Traceback (most recent call last)", "assistant"),
|
|
144
|
+
ContextItemType.Error,
|
|
145
|
+
);
|
|
146
|
+
assert.equal(
|
|
147
|
+
detectItemType("panic: runtime error", "assistant"),
|
|
148
|
+
ContextItemType.Error,
|
|
149
|
+
);
|
|
150
|
+
assert.equal(
|
|
151
|
+
detectItemType("E4030 connection refused", "assistant"),
|
|
152
|
+
ContextItemType.Error,
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("detectItemType: decision content → Decision", () => {
|
|
157
|
+
assert.equal(
|
|
158
|
+
detectItemType("we decided to use JWT auth", "assistant"),
|
|
159
|
+
ContextItemType.Decision,
|
|
160
|
+
);
|
|
161
|
+
assert.equal(
|
|
162
|
+
detectItemType("going with option B", "assistant"),
|
|
163
|
+
ContextItemType.Decision,
|
|
164
|
+
);
|
|
165
|
+
assert.equal(
|
|
166
|
+
detectItemType("let's go with the async approach", "assistant"),
|
|
167
|
+
ContextItemType.Decision,
|
|
168
|
+
);
|
|
169
|
+
assert.equal(
|
|
170
|
+
detectItemType("switching to a different database", "assistant"),
|
|
171
|
+
ContextItemType.Decision,
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("detectItemType: fenced code block ≥20 chars → CodeBlock", () => {
|
|
176
|
+
const code = "```js\nconst x = 42;\nconst y = 'hello world';\n```";
|
|
177
|
+
assert.equal(
|
|
178
|
+
detectItemType(code, "assistant"),
|
|
179
|
+
ContextItemType.CodeBlock,
|
|
180
|
+
);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("detectItemType: short fenced block (<20 chars) does NOT → CodeBlock", () => {
|
|
184
|
+
const short = "```\nshort\n```";
|
|
185
|
+
assert.notEqual(
|
|
186
|
+
detectItemType(short, "assistant"),
|
|
187
|
+
ContextItemType.CodeBlock,
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("detectItemType: file modification verbs → FileModification", () => {
|
|
192
|
+
assert.equal(
|
|
193
|
+
detectItemType("edited src/config.ts to add a flag", "assistant"),
|
|
194
|
+
ContextItemType.FileModification,
|
|
195
|
+
);
|
|
196
|
+
assert.equal(
|
|
197
|
+
detectItemType("created README.md", "assistant"),
|
|
198
|
+
ContextItemType.FileModification,
|
|
199
|
+
);
|
|
200
|
+
assert.equal(
|
|
201
|
+
detectItemType("wrote tests/main.test.ts", "assistant"),
|
|
202
|
+
ContextItemType.FileModification,
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("detectItemType: role=user → UserMessage", () => {
|
|
207
|
+
assert.equal(
|
|
208
|
+
detectItemType("plain question", "user"),
|
|
209
|
+
ContextItemType.UserMessage,
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("detectItemType: role=assistant → AssistantMessage", () => {
|
|
214
|
+
assert.equal(
|
|
215
|
+
detectItemType("plain answer", "assistant"),
|
|
216
|
+
ContextItemType.AssistantMessage,
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("detectItemType: fallback → AssistantMessage", () => {
|
|
221
|
+
// Unknown role with non-matching content falls back to AssistantMessage.
|
|
222
|
+
assert.equal(
|
|
223
|
+
detectItemType("nothing matches here", "unknown" as any),
|
|
224
|
+
ContextItemType.AssistantMessage,
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("detectItemType: empty string does not crash and falls back", () => {
|
|
229
|
+
assert.equal(detectItemType("", "user"), ContextItemType.UserMessage);
|
|
230
|
+
assert.equal(detectItemType("", "assistant"), ContextItemType.AssistantMessage);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("detectItemType: priority — error before decision (first match wins)", () => {
|
|
234
|
+
// A message that contains BOTH "error" and "decided" — error wins (rule 3 before rule 4).
|
|
235
|
+
assert.equal(
|
|
236
|
+
detectItemType("error: we decided to fail", "assistant"),
|
|
237
|
+
ContextItemType.Error,
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ---- S40A-6: score ----
|
|
242
|
+
|
|
243
|
+
test("score: fresh decision gets full multiplier (no decay, no recency/retention boost expected unless young)", () => {
|
|
244
|
+
const result = score(
|
|
245
|
+
{
|
|
246
|
+
id: "0",
|
|
247
|
+
content: "we decided to use JWT",
|
|
248
|
+
role: "assistant",
|
|
249
|
+
timestamp: NOW, // age 0
|
|
250
|
+
},
|
|
251
|
+
NOW,
|
|
252
|
+
);
|
|
253
|
+
assert.equal(result.type, ContextItemType.Decision);
|
|
254
|
+
assert.equal(result.rawMultiplier, 2.5);
|
|
255
|
+
assert.equal(result.ageDecay, 0);
|
|
256
|
+
assert.equal(result.recencyBoost, 1.2); // age 0 < 5min
|
|
257
|
+
assert.equal(result.retentionBoost, 1.0); // not userFlagged
|
|
258
|
+
// 2.5 * (1 - 0) * 1.2 * 1.0 = 3.0
|
|
259
|
+
assert.equal(result.finalScore, 3.0);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test("score: old decision decays", () => {
|
|
263
|
+
const tenHoursAgo = NOW - 10 * 3_600_000;
|
|
264
|
+
const result = score(
|
|
265
|
+
{
|
|
266
|
+
id: "1",
|
|
267
|
+
content: "we decided to use JWT",
|
|
268
|
+
role: "assistant",
|
|
269
|
+
timestamp: tenHoursAgo,
|
|
270
|
+
},
|
|
271
|
+
NOW,
|
|
272
|
+
);
|
|
273
|
+
// decay at 10h = 0.5; recency 1.0 (>5min); retention 1.0
|
|
274
|
+
// 2.5 * (1 - 0.5) * 1.0 * 1.0 = 1.25
|
|
275
|
+
assert.equal(result.ageDecay, 0.5);
|
|
276
|
+
assert.equal(result.recencyBoost, 1.0);
|
|
277
|
+
assert.equal(result.finalScore, 1.25);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("score: userFlagged triples the result via retentionBoost", () => {
|
|
281
|
+
const flagged = score(
|
|
282
|
+
{
|
|
283
|
+
id: "2",
|
|
284
|
+
content: "we decided to use JWT",
|
|
285
|
+
role: "assistant",
|
|
286
|
+
timestamp: NOW,
|
|
287
|
+
userFlagged: true,
|
|
288
|
+
},
|
|
289
|
+
NOW,
|
|
290
|
+
);
|
|
291
|
+
const unflagged = score(
|
|
292
|
+
{
|
|
293
|
+
id: "3",
|
|
294
|
+
content: "we decided to use JWT",
|
|
295
|
+
role: "assistant",
|
|
296
|
+
timestamp: NOW,
|
|
297
|
+
userFlagged: false,
|
|
298
|
+
},
|
|
299
|
+
NOW,
|
|
300
|
+
);
|
|
301
|
+
// 3.0 / 1.0 retention ratio.
|
|
302
|
+
assert.equal(flagged.finalScore / unflagged.finalScore, 3.0);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("score: clamps finalScore to minimum 0.01", () => {
|
|
306
|
+
// A system message (0.5x) at 14h+ (max decay 0.7) → 0.5 * 0.3 * 1.0 * 1.0 = 0.15. Above 0.01.
|
|
307
|
+
// Force below 0.01 with a custom multiplier of 0 and max decay.
|
|
308
|
+
const result = score(
|
|
309
|
+
{
|
|
310
|
+
id: "4",
|
|
311
|
+
content: "system filler",
|
|
312
|
+
role: "custom",
|
|
313
|
+
timestamp: NOW - 100 * 3_600_000,
|
|
314
|
+
},
|
|
315
|
+
NOW,
|
|
316
|
+
{ [ContextItemType.SystemMessage]: 0.01 },
|
|
317
|
+
{ maxDecay: 0.99 },
|
|
318
|
+
);
|
|
319
|
+
// 0.01 * (1 - 0.99) * 1.0 * 1.0 = 0.0001 → clamped to 0.01.
|
|
320
|
+
assert.equal(result.finalScore, 0.01);
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("score: custom multipliers override DEFAULT_MULTIPLIERS", () => {
|
|
324
|
+
const result = score(
|
|
325
|
+
{
|
|
326
|
+
id: "5",
|
|
327
|
+
content: "we decided to use JWT",
|
|
328
|
+
role: "assistant",
|
|
329
|
+
timestamp: NOW,
|
|
330
|
+
},
|
|
331
|
+
NOW,
|
|
332
|
+
{ [ContextItemType.Decision]: 10.0 },
|
|
333
|
+
);
|
|
334
|
+
assert.equal(result.rawMultiplier, 10.0);
|
|
335
|
+
assert.equal(result.finalScore, 10.0 * 1.2); // 10 * (1-0) * 1.2 * 1.0
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("score: unknown role narrows to assistant defensively", () => {
|
|
339
|
+
const result = score(
|
|
340
|
+
{
|
|
341
|
+
id: "6",
|
|
342
|
+
content: "plain text",
|
|
343
|
+
role: "weird-role" as any,
|
|
344
|
+
timestamp: NOW,
|
|
345
|
+
},
|
|
346
|
+
NOW,
|
|
347
|
+
);
|
|
348
|
+
assert.equal(result.role, "assistant");
|
|
349
|
+
assert.equal(result.type, ContextItemType.AssistantMessage);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("score: undefined/null content does not crash", () => {
|
|
353
|
+
const r1 = score(
|
|
354
|
+
{ id: "a", content: undefined as any, role: "user", timestamp: NOW },
|
|
355
|
+
NOW,
|
|
356
|
+
);
|
|
357
|
+
const r2 = score(
|
|
358
|
+
{ id: "b", content: null as any, role: "assistant", timestamp: NOW },
|
|
359
|
+
NOW,
|
|
360
|
+
);
|
|
361
|
+
assert.equal(r1.type, ContextItemType.UserMessage);
|
|
362
|
+
assert.equal(r2.type, ContextItemType.AssistantMessage);
|
|
363
|
+
assert.equal(r1.content, "");
|
|
364
|
+
assert.equal(r2.content, "");
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("score: determinism — same inputs → same output", () => {
|
|
368
|
+
const item = {
|
|
369
|
+
id: "d1",
|
|
370
|
+
content: "we decided to use JWT",
|
|
371
|
+
role: "assistant",
|
|
372
|
+
timestamp: NOW - 3_600_000,
|
|
373
|
+
};
|
|
374
|
+
const a = score(item, NOW);
|
|
375
|
+
const b = score(item, NOW);
|
|
376
|
+
assert.deepEqual(a, b);
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
// ---- S40A-7: preservationCutoff + itemsToPreserve ----
|
|
380
|
+
|
|
381
|
+
test("preservationCutoff: 10 items at ratio 0.3 → threshold is the 3rd-highest score", () => {
|
|
382
|
+
const items = Array.from({ length: 10 }, (_, i) => ({
|
|
383
|
+
id: String(i),
|
|
384
|
+
type: ContextItemType.AssistantMessage,
|
|
385
|
+
content: "",
|
|
386
|
+
role: "assistant",
|
|
387
|
+
timestamp: 0,
|
|
388
|
+
rawMultiplier: 1.0,
|
|
389
|
+
ageDecay: 0,
|
|
390
|
+
recencyBoost: 1.0,
|
|
391
|
+
retentionBoost: 1.0,
|
|
392
|
+
// scores 1.0, 2.0, ..., 10.0
|
|
393
|
+
finalScore: (i + 1) * 1.0,
|
|
394
|
+
}));
|
|
395
|
+
const threshold = preservationCutoff(items, 0.3);
|
|
396
|
+
// top 30% of 10 = 3 items; the 3rd highest is 8.0 (scores 10,9,8 preserved).
|
|
397
|
+
assert.equal(threshold, 8.0);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("preservationCutoff: ratio=1.0 returns 0 (preserve all)", () => {
|
|
401
|
+
const items = [{ finalScore: 5, id: "x" } as any];
|
|
402
|
+
assert.equal(preservationCutoff(items, 1.0), 0);
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("preservationCutoff: ratio=0 returns Infinity (preserve none)", () => {
|
|
406
|
+
const items = [{ finalScore: 5, id: "x" } as any];
|
|
407
|
+
assert.equal(preservationCutoff(items, 0), Infinity);
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("preservationCutoff: empty input returns Infinity", () => {
|
|
411
|
+
assert.equal(preservationCutoff([], 0.5), Infinity);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test("itemsToPreserve: returns IDs at or above threshold", () => {
|
|
415
|
+
const items = Array.from({ length: 10 }, (_, i) => ({
|
|
416
|
+
id: String(i),
|
|
417
|
+
type: ContextItemType.AssistantMessage,
|
|
418
|
+
content: "",
|
|
419
|
+
role: "assistant",
|
|
420
|
+
timestamp: 0,
|
|
421
|
+
rawMultiplier: 1.0,
|
|
422
|
+
ageDecay: 0,
|
|
423
|
+
recencyBoost: 1.0,
|
|
424
|
+
retentionBoost: 1.0,
|
|
425
|
+
finalScore: (i + 1) * 1.0,
|
|
426
|
+
}));
|
|
427
|
+
const result = itemsToPreserve(items, 0.3);
|
|
428
|
+
assert.equal(result.totalScored, 10);
|
|
429
|
+
assert.equal(result.totalPreserved, 3);
|
|
430
|
+
assert.deepEqual([...result.preservedIds].sort(), ["7", "8", "9"]);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("itemsToPreserve: ratio=1.0 preserves everything", () => {
|
|
434
|
+
const items = Array.from({ length: 5 }, (_, i) => ({
|
|
435
|
+
id: String(i),
|
|
436
|
+
type: ContextItemType.AssistantMessage,
|
|
437
|
+
content: "",
|
|
438
|
+
role: "assistant",
|
|
439
|
+
timestamp: 0,
|
|
440
|
+
rawMultiplier: 1.0,
|
|
441
|
+
ageDecay: 0,
|
|
442
|
+
recencyBoost: 1.0,
|
|
443
|
+
retentionBoost: 1.0,
|
|
444
|
+
finalScore: i + 1,
|
|
445
|
+
}));
|
|
446
|
+
const result = itemsToPreserve(items, 1.0);
|
|
447
|
+
assert.equal(result.totalPreserved, 5);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
test("itemsToPreserve: ratio=0 preserves nothing", () => {
|
|
451
|
+
const items = Array.from({ length: 5 }, (_, i) => ({
|
|
452
|
+
id: String(i),
|
|
453
|
+
type: ContextItemType.AssistantMessage,
|
|
454
|
+
content: "",
|
|
455
|
+
role: "assistant",
|
|
456
|
+
timestamp: 0,
|
|
457
|
+
rawMultiplier: 1.0,
|
|
458
|
+
ageDecay: 0,
|
|
459
|
+
recencyBoost: 1.0,
|
|
460
|
+
retentionBoost: 1.0,
|
|
461
|
+
finalScore: i + 1,
|
|
462
|
+
}));
|
|
463
|
+
const result = itemsToPreserve(items, 0);
|
|
464
|
+
assert.equal(result.totalPreserved, 0);
|
|
465
|
+
assert.equal(result.threshold, Infinity);
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
test("itemsToPreserve: empty items returns empty set", () => {
|
|
469
|
+
const result = itemsToPreserve([], 0.5);
|
|
470
|
+
assert.equal(result.totalScored, 0);
|
|
471
|
+
assert.equal(result.totalPreserved, 0);
|
|
472
|
+
assert.equal(result.preservedIds.size, 0);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// ---- scoreEngineMessages: position-based age fallback ----
|
|
476
|
+
|
|
477
|
+
test("scoreEngineMessages: assigns position-based timestamps (oldest first)", () => {
|
|
478
|
+
const messages: EngineMessage[] = [
|
|
479
|
+
{ role: "assistant", text: "we decided to use JWT" },
|
|
480
|
+
{ role: "user", text: "ok" },
|
|
481
|
+
{ role: "assistant", text: "edited src/config.ts" },
|
|
482
|
+
];
|
|
483
|
+
const scored = scoreEngineMessages(messages, NOW);
|
|
484
|
+
assert.equal(scored.length, 3);
|
|
485
|
+
// Oldest (i=0) is `messages.length` minutes ago = 3 min.
|
|
486
|
+
assert.equal(scored[0].timestamp, NOW - 3 * 60_000);
|
|
487
|
+
// Newest (i=2) is 1 min ago.
|
|
488
|
+
assert.equal(scored[2].timestamp, NOW - 1 * 60_000);
|
|
489
|
+
// All recency-boosted (< 5 min).
|
|
490
|
+
assert.equal(scored[0].recencyBoost, 1.2);
|
|
491
|
+
assert.equal(scored[2].recencyBoost, 1.2);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("scoreEngineMessages: preserves input order and ids are stringified indices", () => {
|
|
495
|
+
const messages: EngineMessage[] = [
|
|
496
|
+
{ role: "user", text: "first" },
|
|
497
|
+
{ role: "user", text: "second" },
|
|
498
|
+
];
|
|
499
|
+
const scored = scoreEngineMessages(messages, NOW);
|
|
500
|
+
assert.equal(scored[0].id, "0");
|
|
501
|
+
assert.equal(scored[1].id, "1");
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
test("scoreEngineMessages: empty input returns empty array", () => {
|
|
505
|
+
assert.deepEqual(scoreEngineMessages([], NOW), []);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
test("scoreEngineMessages: determinism — same input → same output", () => {
|
|
509
|
+
const messages: EngineMessage[] = [
|
|
510
|
+
{ role: "assistant", text: "we decided to use JWT" },
|
|
511
|
+
{ role: "user", text: "ok" },
|
|
512
|
+
];
|
|
513
|
+
const a = scoreEngineMessages(messages, NOW);
|
|
514
|
+
const b = scoreEngineMessages(messages, NOW);
|
|
515
|
+
assert.deepEqual(a, b);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
// ---- integration: a decision outsurvives filler ----
|
|
519
|
+
|
|
520
|
+
test("integration: a decision is preserved over filler at the same age", () => {
|
|
521
|
+
// 10 messages, 1 decision (i=4) and 9 filler, all equally old.
|
|
522
|
+
const messages: EngineMessage[] = Array.from({ length: 10 }, (_, i) =>
|
|
523
|
+
i === 4
|
|
524
|
+
? { role: "assistant", text: "we decided to use JWT auth" }
|
|
525
|
+
: { role: "assistant", text: "ok, sounds good" },
|
|
526
|
+
);
|
|
527
|
+
const scored = scoreEngineMessages(messages, NOW - 10 * 60_000); // 10 min old → no recency boost
|
|
528
|
+
// decision rawMultiplier 2.5 vs filler 1.0; same decay, same recency.
|
|
529
|
+
const decision = scored[4];
|
|
530
|
+
const filler = scored[0];
|
|
531
|
+
assert.ok(decision.finalScore > filler.finalScore);
|
|
532
|
+
// top 20% → 2 items; the decision must be in the preserved set.
|
|
533
|
+
const result = itemsToPreserve(scored, 0.2);
|
|
534
|
+
assert.ok(
|
|
535
|
+
result.preservedIds.has("4"),
|
|
536
|
+
"decision must be preserved",
|
|
537
|
+
);
|
|
538
|
+
});
|