dsh-lite-memory 0.18.43 → 0.18.44

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.
@@ -1,324 +0,0 @@
1
- import assert from 'node:assert/strict';
2
- import fs from 'node:fs';
3
- import os from 'node:os';
4
- import path from 'node:path';
5
-
6
- process.env.DSH_HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-control-'));
7
- const { apply } = await import('../index.js');
8
- const store = await import('../store.js');
9
- const state = await import('../state.js');
10
-
11
- const effect = (fn) => {
12
- const dispose = fn();
13
- return typeof dispose === 'function' ? dispose : () => {};
14
- };
15
-
16
- function boot() {
17
- const defs = new Map();
18
- const handlers = new Map();
19
- const promptContexts = [];
20
- const ctx = {
21
- tools: { register: (def) => { defs.set(def.name, def); return () => {}; } },
22
- effect,
23
- inject: () => () => {},
24
- get: (name) => (name === 'systemPrompt'
25
- ? { context: (entry) => { promptContexts.push(entry); return () => {}; } }
26
- : name === 'agents' ? { currentInitiator: () => null } : undefined),
27
- on: (event, handler) => {
28
- const list = handlers.get(event) || [];
29
- list.push(handler);
30
- handlers.set(event, list);
31
- return () => {};
32
- },
33
- };
34
- apply(ctx, { mode: 'adaptive' });
35
- const provider = promptContexts.find((entry) => entry.name === 'lite-memory:recall');
36
- /* CI 冷启动偶尔超过召回耗时上限,逻辑断言不该被「这轮先不注入」判失败。 */
37
- globalThis.__liteMemoryRecall?.setDeadlineMs?.(5000);
38
- const exec = (name, args, agent) => defs.get(name).execute(args, { agent });
39
- const preStep = (payload, decision) => handlers.get('agent/pre-step').at(-1)(payload, async () => decision);
40
- return { exec, preStep, text: (agent, signal) => provider.text({ agent, signal }), debug: globalThis.__liteMemoryRecall };
41
- }
42
-
43
- let seq = 0;
44
- const human = (body) => { seq += 1; return { role: 'user', id: `msg-${seq}`, source: { kind: 'user' }, content: [{ type: 'text', text: body }] }; };
45
- const enter = (...messages) => ({ kind: 'enter', messages });
46
- const mkdir = (label) => fs.mkdtempSync(path.join(os.tmpdir(), `lite-memory-control-${label}-`));
47
- const makeAgent = (id, dir) => ({ id, cwd: dir, session: { id: `session-${id}`, header: { cwd: dir } } });
48
- const save = (scope, fields) => store.upsert(scope, {
49
- applicability: 'all_in_scope',
50
- body: `正文:${fields.title}`,
51
- resolution: 'distinct',
52
- ...fields,
53
- summary: fields.summary ?? `${fields.title}的摘要`,
54
- });
55
- const seedRule = (scope, title) => {
56
- const saved = save(scope, { title, tags: ['口播'] });
57
- const id = saved.record?.id ?? saved.id;
58
- state.transact(scope, {}, (draft) => {
59
- const row = draft.entries.find((entry) => entry.id === id);
60
- row.origin = 'project_evolution';
61
- row.current.conditions = [{ key: 'seriesId', value: '口播' }];
62
- row.maintenance.learning = { state: 'active', specDigest: 'seeded' };
63
- return { changed: true };
64
- });
65
- return id;
66
- };
67
- const scopeOf = (dir) => store.scopeOf(dir);
68
- const toggle = (scope, enabled) => store.setMemoryEnabled(scope, enabled, { operationId: `op-${Math.random().toString(16).slice(2)}` });
69
-
70
- /* C00:触发方式按项目存,切到「仅搜索」不改总开关,关闭时也不偷偷恢复授权。 */
71
- {
72
- const scope = scopeOf(mkdir('mode'));
73
- assert.equal(store.controlStatus(scope).enabled, true);
74
- assert.equal(store.controlStatus(scope).mode, '');
75
- const epochBefore = store.controlStatus(scope).epoch;
76
- const searched = store.setRecallMode(scope, 'tool_only', { operationId: 'op-mode-search' });
77
- assert.equal(searched.ok, true);
78
- assert.equal(searched.mode, 'tool_only');
79
- assert.equal(store.controlStatus(scope).enabled, true, '只改触发方式,不动总开关');
80
- assert.notEqual(store.controlStatus(scope).epoch, epochBefore, '改触发方式要换掉旧回执的凭据');
81
- assert.equal(store.setRecallMode(scope, '自动', { operationId: 'op-mode-bad' }).ok, false);
82
- assert.equal(store.controlStatus(scope).mode, 'tool_only', '不认识的值不落盘');
83
- assert.equal(toggle(scope, false).ok, true);
84
- assert.equal(store.setRecallMode(scope, 'adaptive', { operationId: 'op-mode-off' }).mode, 'adaptive');
85
- assert.equal(store.controlStatus(scope).enabled, false, '关闭时改触发方式不能顺手打开');
86
- const combined = store.setMemoryControl(scope, { enabled: true, mode: 'tool_only' }, { operationId: 'op-mode-combined' });
87
- assert.equal(combined.ok, true);
88
- assert.equal(combined.enabled, true);
89
- assert.equal(combined.mode, 'tool_only');
90
- }
91
-
92
- /* C01:总开关关闭后,自动注入、全局召回、检索、读取、写入、学习全部停。 */
93
- {
94
- const dir = mkdir('off');
95
- const scope = scopeOf(dir);
96
- const globalSaved = save(store.globalScope(), { title: '全局写作偏好', tags: ['写作'] });
97
- const globalId = globalSaved.record?.id ?? globalSaved.id;
98
- save(scope, { title: '项目构建约定', tags: ['构建'] });
99
- seedRule(scope, '口播开头');
100
- assert.equal(store.setEvolution(scope, true, { operationId: 'evo-on' }).ok, true);
101
- assert.equal(toggle(scope, false).ok, true);
102
- assert.equal(store.controlStatus(scope).enabled, false);
103
-
104
- const runtime = boot();
105
- const agent = makeAgent('a-off', dir);
106
- const sig = new AbortController().signal;
107
- await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('按全局写作偏好来修构建脚本')));
108
- assert.equal(runtime.text(agent, sig), '');
109
- assert.equal(runtime.debug.preparedOf(agent), null);
110
-
111
- const found = await runtime.exec('memory_search', { query: '写作 构建 口播开头' }, agent);
112
- assert.equal(found.status, 'memory_disabled');
113
- assert.equal(found.count, 0);
114
- assert.ok(globalId);
115
- assert.equal((await runtime.exec('memory_read', { id: globalId }, agent)).status, 'memory_disabled');
116
- assert.equal((await runtime.exec('memory_forget', { id: globalId, mode: 'archive' }, agent)).mode, 'memory_disabled');
117
- const suggested = await runtime.exec('memory_suggest', { title: '新偏好', summary: '一条偏好' }, agent);
118
- assert.equal(suggested.asked, false);
119
- const saved = await runtime.exec('memory_save', { title: '关闭后写入', summary: '应当拒绝', body: '不该落盘' }, agent);
120
- assert.equal(saved.changed, false);
121
- assert.equal(saved.reason, 'memory_disabled');
122
- assert.equal(saved.id, undefined);
123
- }
124
-
125
- /* C02:关闭期间所有写入入口都拒绝,自进化不能绕过总开关打开。 */
126
- {
127
- const dir = mkdir('write');
128
- const scope = scopeOf(dir);
129
- assert.equal(toggle(scope, false).ok, true);
130
- assert.equal(store.setEvolution(scope, true, { operationId: 'evo-blocked' }).ok, false);
131
- const saved = store.saveEntry(scope, { title: '一条项目规范', summary: '普通摘要', body: '正文', kind: 'convention', scope: 'project', durability: 'durable' });
132
- assert.equal(saved.reasonCode, 'memory_disabled');
133
- const task = store.saveTaskState(scope, { version: 1, sessions: {}, episodes: {}, defaults: null, revision: 1 }, { operationId: 'task-blocked' });
134
- assert.equal(task.ok, false);
135
- const committed = store.commitLearning(scope, { title: '学习规则', summary: '摘要', body: '正文' }, {}, { operationId: 'learn-blocked' });
136
- assert.equal(committed.ok, false);
137
- const run = store.setEvolution(scope, false, { operationId: 'evo-off' });
138
- assert.equal(run.ok, false);
139
- assert.equal(run.code, 'memory_disabled');
140
- assert.equal(store.controlStatus(scope).enabled, false);
141
- }
142
-
143
- /* C03:关一个项目不影响另一个;重新打开记忆后自进化仍保持关闭。 */
144
- {
145
- const dirA = mkdir('iso-a');
146
- const dirB = mkdir('iso-b');
147
- const scopeA = scopeOf(dirA);
148
- const scopeB = scopeOf(dirB);
149
- save(scopeB, { title: 'B 项目构建约定', tags: ['构建', '脚本'] });
150
- seedRule(scopeB, '口播开头');
151
- assert.equal(store.setEvolution(scopeB, true, { operationId: 'evo-b' }).ok, true);
152
- assert.equal(toggle(scopeA, false).ok, true);
153
- assert.equal(store.evolutionStatus(scopeB).enabled, true);
154
- assert.equal(store.controlStatus(scopeB).enabled, true);
155
-
156
- const runtime = boot();
157
- const agentB = makeAgent('b-live', dirB);
158
- const sigB = new AbortController().signal;
159
- await runtime.preStep({ agent: agentB, signal: sigB, turn: 't1' }, enter(human('按项目构建约定改脚本')));
160
- assert.match(runtime.text(agentB, sigB), /B 项目构建约定/);
161
-
162
- toggle(scopeB, false);
163
- assert.equal(store.evolutionStatus(scopeB).enabled, false);
164
- assert.equal(store.evolutionStatus(scopeB).configuredEnabled, false);
165
- toggle(scopeB, true);
166
- assert.equal(store.evolutionStatus(scopeB).enabled, false);
167
- assert.equal(store.setEvolution(scopeB, true, { operationId: 'evo-b-again' }).ok, true);
168
- assert.equal(store.evolutionStatus(scopeB).enabled, true);
169
- }
170
-
171
- /* C04:切换总开关让旧任务上下文立即失效,不拿关闭前的绑定继续注入。 */
172
- {
173
- const dir = mkdir('stale');
174
- const scope = scopeOf(dir);
175
- seedRule(scope, '口播开头');
176
- assert.equal(store.setEvolution(scope, true, { operationId: 'evo-stale' }).ok, true);
177
- const runtime = boot();
178
- const agent = makeAgent('a-stale', dir);
179
- const sig = new AbortController().signal;
180
- await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('这期口播开头怎么改')));
181
- const held = runtime.debug.windowOf(agent).experiment;
182
- assert.ok(held && held.contextId);
183
- toggle(scope, false);
184
- toggle(scope, true);
185
- const stale = await runtime.exec('memory_search', { query: '口播开头', contextId: held.contextId }, agent);
186
- assert.equal(stale.status !== 'ok', true);
187
- assert.equal((await runtime.exec('memory_search', { query: '口播开头' }, agent)).status, 'ok');
188
- }
189
-
190
- /* C05:关闭保留候选、试用、结果与整理回执;重开记忆后自进化仍是关闭。 */
191
- {
192
- const dir = mkdir('preserve');
193
- const scope = scopeOf(dir);
194
- const id = seedRule(scope, '口播开头');
195
- assert.equal(store.setEvolution(scope, true, { operationId: 'evo-preserve' }).ok, true);
196
- const before = store.learningSnapshot(scope);
197
- assert.equal(before.counts.active, 1);
198
- assert.equal(toggle(scope, false).ok, true);
199
- const closed = store.learningSnapshot(scope);
200
- assert.equal(closed.enabled, false);
201
- assert.equal(closed.rules.length, before.rules.length);
202
- assert.equal(closed.counts.active, 1);
203
- assert.equal(toggle(scope, true).ok, true);
204
- const reopened = store.learningSnapshot(scope);
205
- assert.equal(reopened.enabled, false);
206
- assert.equal(reopened.counts.active, 1);
207
- assert.equal(store.evolutionStatus(scope).configuredEnabled, false);
208
- assert.equal(store.setEvolution(scope, true, { operationId: 'evo-resume' }).ok, true);
209
- assert.equal(store.learningSnapshot(scope).counts.active, 1);
210
- assert.ok(id);
211
- }
212
-
213
- /* C06:关闭期间管理面写操作一律拒绝,只读快照不写盘。 */
214
- {
215
- const dir = mkdir('managed-off');
216
- const scope = scopeOf(dir);
217
- const id = seedRule(scope, '口播开头');
218
- assert.equal(store.setEvolution(scope, true, { operationId: 'evo-managed' }).ok, true);
219
- toggle(scope, false);
220
- const file = state.stateFiles(scope).current;
221
- const before = fs.readFileSync(file);
222
- const snapshot = store.learningSnapshot(scope);
223
- const rule = snapshot.rules[0];
224
- assert.equal(rule.id, id);
225
- assert.equal(Buffer.compare(before, fs.readFileSync(file)), 0);
226
- for (const action of ['pin', 'pause', 'resume', 'delete', 'undo', 'adopt', 'edit']) {
227
- let result;
228
- if (action === 'pin') result = store.pinLearningRule(scope, id, { pinned: true, expectedRevision: rule.revision });
229
- if (action === 'pause') result = store.pauseLearningRules(scope, [id], { operationId: 'pause-off' });
230
- if (action === 'resume') result = store.resumeLearningRule(scope, id);
231
- if (action === 'delete') result = store.removeLearningRule(scope, id, { expectedRevision: rule.revision });
232
- if (action === 'undo') result = store.undoLearningChange(scope, id, { expectedRevision: rule.revision });
233
- if (action === 'adopt') result = store.adoptLearningRule(scope, id, { expectedRevision: rule.revision });
234
- if (action === 'edit') result = store.editLearningRule(scope, id, { expectedRevision: rule.revision, body: '改过的正文' });
235
- assert.equal(result.ok, false, action);
236
- assert.equal(result.code, 'memory_disabled', action);
237
- }
238
- assert.equal(Buffer.compare(before, fs.readFileSync(file)), 0);
239
- }
240
-
241
- /* C07:采纳把候选转为生效;编辑只保存并留上一版,不改状态以外的东西。 */
242
- {
243
- const dir = mkdir('adopt');
244
- const scope = scopeOf(dir);
245
- const id = seedRule(scope, '口播开头');
246
- const first = store.learningSnapshot(scope).rules[0];
247
- const edited = store.editLearningRule(scope, id, { expectedRevision: first.revision, body: '开头两句内给结论。', title: '口播结论前置' });
248
- assert.equal(edited.ok, true);
249
- const afterEdit = store.learningSnapshot(scope).rules[0];
250
- assert.equal(afterEdit.state, 'trial');
251
- assert.equal(afterEdit.body, '开头两句内给结论。');
252
- assert.equal(afterEdit.title, '口播结论前置');
253
- assert.equal(afterEdit.learning.versions.length, 1);
254
- const adopted = store.adoptLearningRule(scope, id, { expectedRevision: afterEdit.revision });
255
- assert.equal(adopted.ok, true);
256
- const afterAdopt = store.learningSnapshot(scope).rules[0];
257
- assert.equal(afterAdopt.state, 'active');
258
- assert.equal(afterAdopt.body, '开头两句内给结论。');
259
- assert.equal(afterAdopt.title, '口播结论前置');
260
- }
261
-
262
- /* C07b:只改标题不动正文时保留生效状态与学习依据。 */
263
- {
264
- const dir = mkdir('title-only');
265
- const scope = scopeOf(dir);
266
- const id = seedRule(scope, '口播开头');
267
- state.transact(scope, {}, (draft) => {
268
- const row = draft.entries.find((entry) => entry.id === id);
269
- row.maintenance.learning = { state: 'active', evidence: [{ text: '三次口播都先改开头' }] };
270
- return { changed: true };
271
- });
272
- const first = store.learningSnapshot(scope).rules[0];
273
- const edited = store.editLearningRule(scope, id, { expectedRevision: first.revision, title: '口播开头重命名' });
274
- assert.equal(edited.ok, true);
275
- const after = store.learningSnapshot(scope).rules[0];
276
- assert.equal(after.state, 'active');
277
- assert.equal(after.learning.evidence.length, 1);
278
- assert.equal(after.title, '口播开头重命名');
279
- }
280
-
281
- /* C08:关闭保留已消耗额度与默认绑定,重开后仍按原额度限额。 */
282
- {
283
- const dir = mkdir('preserve-quota');
284
- const scope = scopeOf(dir);
285
- assert.equal(store.setEvolution(scope, true, { operationId: 'evo-quota' }).ok, true);
286
- const defaults = { taskKind: 'video', seriesId: '口播' };
287
- assert.equal(store.saveTaskState(scope, { version: 1, sessions: {}, episodes: {}, defaults, revision: 1 }, { operationId: 'defaults' }).ok, true);
288
- assert.equal(store.beginLearningAttempt(scope, { episodeId: 'ep-preserve', operationId: 'a1' }).ok, true);
289
- assert.equal(store.beginLearningAttempt(scope, { episodeId: 'ep-preserve', operationId: 'a2' }).ok, true);
290
- assert.equal(store.beginLearningAttempt(scope, { episodeId: 'ep-preserve', operationId: 'a3' }).ok, false);
291
- const ledgerBefore = store.loadIndex(scope).__evolution.learning.recent.length;
292
- assert.ok(ledgerBefore >= 2, '先有滚动额度记录');
293
- toggle(scope, false);
294
- toggle(scope, true);
295
- assert.ok(store.loadIndex(scope).__evolution.learning.recent.length >= ledgerBefore, '关开不重置滚动额度账本');
296
- assert.deepEqual(store.taskState(scope).defaults, { ...defaults, confirmedBy: 'user', confirmedAt: '' });
297
- assert.equal(store.setEvolution(scope, true, { operationId: 'evo-quota-again' }).ok, true);
298
- const limited = store.beginLearningAttempt(scope, { episodeId: 'ep-preserve', operationId: 'a4' });
299
- assert.equal(limited.ok, false);
300
- assert.equal(limited.reason, 'episode_limit');
301
- }
302
-
303
- /* C09:同一项目两个会话都已准备内容,关闭后两边都立即失效。 */
304
- {
305
- const dir = mkdir('two-sessions');
306
- const scope = scopeOf(dir);
307
- seedRule(scope, '口播开头');
308
- assert.equal(store.setEvolution(scope, true, { operationId: 'evo-two' }).ok, true);
309
- const runtime = boot();
310
- const a1 = makeAgent('two-1', dir);
311
- const a2 = makeAgent('two-2', dir);
312
- const sig = new AbortController().signal;
313
- await runtime.preStep({ agent: a1, signal: sig, turn: 't1' }, enter(human('这期口播开头怎么改')));
314
- await runtime.preStep({ agent: a2, signal: sig, turn: 't1' }, enter(human('这期口播开头怎么改')));
315
- assert.match(runtime.text(a1, sig), /口播开头/);
316
- assert.match(runtime.text(a2, sig), /口播开头/);
317
- toggle(scope, false);
318
- assert.equal(runtime.text(a1, sig), '');
319
- assert.equal(runtime.text(a2, sig), '');
320
- assert.equal(runtime.debug.preparedOf(a1).text, '');
321
- assert.equal(runtime.debug.preparedOf(a2).text, '');
322
- }
323
-
324
- console.log('control.test.mjs: all passed');