dsh-lite-memory 0.18.25 → 0.18.26
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/docs/RELEASE-0.18.26.md +5 -0
- package/index.js +7 -4
- package/package.json +1 -1
- package/store.js +17 -4
- package/test/evolution-read.test.mjs +2 -0
- package/test/state.test.mjs +8 -5
- package/test/store.test.mjs +18 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# 0.18.26
|
|
2
|
+
|
|
3
|
+
- 只在本机成立、但跨项目复用的命令/流程(例如本机 npm 包装命令):声明 machine 或 runtime 条件后可以进全局层,换台机器条件不成立就不会拿出来用;仍然写死本机路径/端口又没声明环境的,继续留在项目层。
|
|
4
|
+
- 显式存全局的降级说明补了一句怎么改:补机器条件后可以存全局,不用把规则改写成一个不存在的通用说法。
|
|
5
|
+
- CI 稳定性:召回投影的耗时上限可以在测试里放宽(CI 冷启动偶尔超过 150ms,逻辑断言不该被「这轮先不注入」判失败);旧库正文比对按行尾归一(Windows 检出把 LF 转成 CRLF)。Windows 与 Ubuntu 六格矩阵全绿。
|
package/index.js
CHANGED
|
@@ -804,7 +804,7 @@ const SAVE_PARAMS = {
|
|
|
804
804
|
enum: ['distinct'],
|
|
805
805
|
description: '明确表示「这确实是另一条」。返回 needsResolution 时,只有确认不是同一件事才带上它再存一次',
|
|
806
806
|
},
|
|
807
|
-
scope: { type: 'string', enum: ['auto', 'project', 'global'], description: '存哪一层:auto 按记忆类别判定(默认);只有用户这一次明确说「以后所有项目都这样」才用 global,模型自己判断通用不是理由;明确说只在这个项目用 project。本机路径/端口和项目事实即使写 global 也会留在项目层,降级原因见 scopeNote' },
|
|
807
|
+
scope: { type: 'string', enum: ['auto', 'project', 'global'], description: '存哪一层:auto 按记忆类别判定(默认);只有用户这一次明确说「以后所有项目都这样」才用 global,模型自己判断通用不是理由;明确说只在这个项目用 project。本机路径/端口和项目事实即使写 global 也会留在项目层,降级原因见 scopeNote;只在本机成立的命令/流程声明 machine 或 runtime 条件后可进全局层' },
|
|
808
808
|
memoryClass: { type: 'string', enum: ['personal_preference', 'project_fact', 'environment_fact', 'portable_procedure'], description: '归属提案:个人偏好 / 项目事实 / 设备事实 / 可搬流程。服务端据此判定存哪层;设备事实必须同时给设备条件' },
|
|
809
809
|
durability: { type: 'string', enum: ['durable', 'task'], description: '一般认为 durable;只是一次性任务参数用 task,服务端会拒绝落盘' },
|
|
810
810
|
evidenceRef: { type: 'string', description: '归属判定的依据(哪条消息、哪个文件、哪次观察);用户没说范围时用来判断能不能存' },
|
|
@@ -1015,7 +1015,8 @@ function statusLabel(status) {
|
|
|
1015
1015
|
*/
|
|
1016
1016
|
export const RECALL_MODES = ['adaptive', 'tool_only', 'off'];
|
|
1017
1017
|
const RECALL_QUERY_CHARS = 400;
|
|
1018
|
-
|
|
1018
|
+
/** 单轮投影的耗时上限:慢机器上宁可这轮不注入,也不把回合拖住。测试可临时放宽。 */
|
|
1019
|
+
let recallDeadlineMs = 150;
|
|
1019
1020
|
const RECALL_HEADER = '已有记忆可能相关(需要细节用 memory_read 带标题取正文,别凭标题猜):';
|
|
1020
1021
|
|
|
1021
1022
|
/** 卡片只放够判断要不要读的几行;必要前提整条在场,装不下就整卡省略。 */
|
|
@@ -1992,7 +1993,7 @@ export function apply(ctx, config) {
|
|
|
1992
1993
|
/** 显式要了全局却被硬冲突挡回项目层时说清原因:否则模型只能看到 scope=project,无从纠。 */
|
|
1993
1994
|
const scopeNote = (() => {
|
|
1994
1995
|
if (args.scope !== 'global' || scope.key === 'global') return '';
|
|
1995
|
-
if (saved.reasonCode === 'machine_bound_scope') return '
|
|
1996
|
+
if (saved.reasonCode === 'machine_bound_scope') return '按本机痕迹留在项目层:正文或依据里写了本机路径/端口,改成别的项目仍成立的通用规则再存全局;只在本机适用的流程可补 machine 条件后存全局';
|
|
1996
1997
|
if (saved.reasonCode === 'model_global_project_fact') return '按项目事实留在项目层:换成别的项目就不成立;确实是通用流程就改 memoryClass';
|
|
1997
1998
|
return '';
|
|
1998
1999
|
})();
|
|
@@ -2706,7 +2707,7 @@ export function apply(ctx, config) {
|
|
|
2706
2707
|
return decision;
|
|
2707
2708
|
}
|
|
2708
2709
|
/** 超时或被取消:这一步不留半旧卡片,下一真人请求重新投影。 */
|
|
2709
|
-
if (signal?.aborted || Date.now() - startedAt >
|
|
2710
|
+
if (signal?.aborted || Date.now() - startedAt > recallDeadlineMs) {
|
|
2710
2711
|
rollbackBudget();
|
|
2711
2712
|
if (snapshot.cards.length) clearRecall(scope, sessionId);
|
|
2712
2713
|
clearPrepared(agent);
|
|
@@ -3044,6 +3045,8 @@ export function apply(ctx, config) {
|
|
|
3044
3045
|
mode: recallMode,
|
|
3045
3046
|
preparedOf: (agent) => prepared.get(agent) || null,
|
|
3046
3047
|
windowOf: (agent) => windowOf(agent),
|
|
3048
|
+
/** 测试专用:CI 冷启动偶尔超过 150ms,逻辑测试不该被耗时上限判失败。 */
|
|
3049
|
+
setDeadlineMs: (ms) => { if (Number.isFinite(ms) && ms > 0) recallDeadlineMs = ms; },
|
|
3047
3050
|
};
|
|
3048
3051
|
return () => {
|
|
3049
3052
|
delete globalThis.__liteMemoryRecall;
|
package/package.json
CHANGED
package/store.js
CHANGED
|
@@ -2520,7 +2520,8 @@ function machineBoundText(source) {
|
|
|
2520
2520
|
|
|
2521
2521
|
/**
|
|
2522
2522
|
* 适用范围判定:内容/依据的普适性优先,模型声明的 scope 只是提案。
|
|
2523
|
-
*
|
|
2523
|
+
* 写死了本机路径/端口又没有声明适用环境时留在项目层;声明了机器/系统条件的
|
|
2524
|
+
* 本机流程可以进全局层——换台机器条件不成立,自然不会拿出来用。
|
|
2524
2525
|
*/
|
|
2525
2526
|
export function routeScope(draft, options = {}) {
|
|
2526
2527
|
const source = draft || {};
|
|
@@ -2532,6 +2533,16 @@ export function routeScope(draft, options = {}) {
|
|
|
2532
2533
|
const evidence = typeof source.evidenceRef === 'string' ? source.evidenceRef.trim() : '';
|
|
2533
2534
|
/** 依据本身是本机文件/路径的,不能算通用流程;正文里带本机痕迹同理。 */
|
|
2534
2535
|
const machineBound = machineBoundText(source) || (evidence ? machineBoundText({ title: evidence }) : false);
|
|
2536
|
+
/** 声明了机器/系统条件,本机痕迹就被这条条件收住:规则仍可搬到所有满足条件的机器上。 */
|
|
2537
|
+
const envScoped = (() => {
|
|
2538
|
+
try {
|
|
2539
|
+
return (view.normalizeConditions(source.conditions) || []).some((item) => ENVIRONMENT_KEYS.includes(item.key));
|
|
2540
|
+
} catch {
|
|
2541
|
+
return false;
|
|
2542
|
+
}
|
|
2543
|
+
})();
|
|
2544
|
+
/** 真正挡回项目层的只有「本机痕迹 + 没有声明适用环境」;只写命令名不算本机痕迹。 */
|
|
2545
|
+
const machineConflict = machineBound && !envScoped;
|
|
2535
2546
|
const base = { chosenScope: '', reasonCode: '', requiredConditions: [], needsScope: false, explicit };
|
|
2536
2547
|
/** 通用层拿不出普适证据时:有项目就留在项目层,没有就问一次运行环境。 */
|
|
2537
2548
|
const fallbackToProject = (reasonCode) => {
|
|
@@ -2553,7 +2564,7 @@ export function routeScope(draft, options = {}) {
|
|
|
2553
2564
|
* 没显式 scope 时才用 memoryClass 提案,缺可搬依据的流程留在项目层。
|
|
2554
2565
|
*/
|
|
2555
2566
|
if (explicit === 'global' && !humanConfirmed) {
|
|
2556
|
-
if (
|
|
2567
|
+
if (machineConflict) return fallbackToProject('machine_bound_scope');
|
|
2557
2568
|
if (cls === 'project_fact') return fallbackToProject('model_global_project_fact');
|
|
2558
2569
|
}
|
|
2559
2570
|
if (explicit === 'project' && !projectReady) {
|
|
@@ -2578,11 +2589,13 @@ export function routeScope(draft, options = {}) {
|
|
|
2578
2589
|
}
|
|
2579
2590
|
if (cls === 'personal_preference') {
|
|
2580
2591
|
/** 偏好也分「我的习惯」和「这台机器上的习惯」:后者绑机器,不进通用层。 */
|
|
2581
|
-
if (
|
|
2592
|
+
if (machineConflict) return fallbackToProject('machine_bound_preference');
|
|
2582
2593
|
return { ...base, chosenScope: 'global', reasonCode: 'class_personal_preference' };
|
|
2583
2594
|
}
|
|
2584
2595
|
if (cls === 'portable_procedure') {
|
|
2585
|
-
if (
|
|
2596
|
+
if (machineConflict) return fallbackToProject('machine_bound_procedure');
|
|
2597
|
+
/** 带适用环境条件的本机流程:条件成立时才用,换台机器自然不生效。 */
|
|
2598
|
+
if (envScoped) return { ...base, chosenScope: 'global', reasonCode: 'machine_scoped_procedure' };
|
|
2586
2599
|
/** 说能搬去别的项目,就要有能搬的依据;没有就先留在本项目。 */
|
|
2587
2600
|
if (!evidence) return fallbackToProject('unbound_evidence');
|
|
2588
2601
|
return { ...base, chosenScope: 'global', reasonCode: 'class_portable_procedure' };
|
|
@@ -35,6 +35,8 @@ function boot(mode = 'adaptive') {
|
|
|
35
35
|
apply(ctx, { mode });
|
|
36
36
|
const provider = promptContexts.find((entry) => entry.name === 'lite-memory:recall');
|
|
37
37
|
assert.ok(provider, 'recall provider registered');
|
|
38
|
+
/* CI 冷启动偶尔超过召回耗时上限,逻辑断言不该被「这轮先不注入」判失败。 */
|
|
39
|
+
globalThis.__liteMemoryRecall?.setDeadlineMs?.(5000);
|
|
38
40
|
const exec = (name, args, agent) => defs.get(name).execute(args, { agent });
|
|
39
41
|
const preStep = (payload, decision) => handlers.get('agent/pre-step').at(-1)(payload, async () => decision);
|
|
40
42
|
const text = (agent, signal) => provider.text({ agent, signal });
|
package/test/state.test.mjs
CHANGED
|
@@ -23,6 +23,11 @@ const test = (name, fn) => {
|
|
|
23
23
|
};
|
|
24
24
|
|
|
25
25
|
const dirs = [];
|
|
26
|
+
/** Windows 会把同一个临时目录报成 8.3 短名或长名;按目录的文件 ID 判断是不是同一处。 */
|
|
27
|
+
const pathId = (target) => {
|
|
28
|
+
const stat = fs.statSync(target, { bigint: true });
|
|
29
|
+
return `${stat.dev}:${stat.ino}`;
|
|
30
|
+
};
|
|
26
31
|
const fresh = (label) => {
|
|
27
32
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), `lm-state-${label}-`));
|
|
28
33
|
dirs.push(dir);
|
|
@@ -218,9 +223,7 @@ test('S01 仓库根与子目录共用一份记忆', () => {
|
|
|
218
223
|
const atSub = scopeOf(sub);
|
|
219
224
|
assert.equal(atSub.key, atRoot.key);
|
|
220
225
|
assert.equal(atSub.dir, atRoot.dir);
|
|
221
|
-
|
|
222
|
-
name on the other; compare what both of them resolve to. */
|
|
223
|
-
assert.equal(fs.realpathSync(resolveRoot(sub).root), fs.realpathSync(repo));
|
|
226
|
+
assert.equal(pathId(resolveRoot(sub).root), pathId(repo));
|
|
224
227
|
});
|
|
225
228
|
|
|
226
229
|
test('S02 符号链接、非 Git 目录、嵌套仓库各归各的', () => {
|
|
@@ -251,7 +254,7 @@ test('S03 配置的项目根只覆盖自己的子树,不吞别的目录', () =
|
|
|
251
254
|
fs.mkdirSync(path.dirname(config), { recursive: true });
|
|
252
255
|
fs.writeFileSync(config, JSON.stringify({ projectRoot: root }));
|
|
253
256
|
try {
|
|
254
|
-
assert.equal(resolveRoot(sub).root,
|
|
257
|
+
assert.equal(pathId(resolveRoot(sub).root), pathId(root));
|
|
255
258
|
assert.equal(resolveRoot(sub).source, 'configured');
|
|
256
259
|
assert.equal(scopeOf(sub).key, scopeOf(root).key);
|
|
257
260
|
assert.notEqual(resolveRoot(outside).source, 'configured');
|
|
@@ -327,7 +330,7 @@ test('S10 旧库迁移:正文、归档、旧标题一起搬进状态', () => {
|
|
|
327
330
|
const loaded = load(scope).state;
|
|
328
331
|
assert.equal(loaded.entries.length, 4);
|
|
329
332
|
assert.equal(loaded.migration.fromVersion, 1);
|
|
330
|
-
assert.equal(bodyOf(scope, 'mem_111111111111'), '第一步跑测试\n第二步改版本号');
|
|
333
|
+
assert.equal(bodyOf(scope, 'mem_111111111111').replace(/\r\n/g, '\n'), '第一步跑测试\n第二步改版本号');
|
|
331
334
|
const archived = loaded.entries.find((entry) => entry.id === 'mem_444444444444');
|
|
332
335
|
assert.equal(archived.status, 'archived');
|
|
333
336
|
assert.equal(archived.current.body, '归档正文');
|
package/test/store.test.mjs
CHANGED
|
@@ -1089,6 +1089,24 @@ test('归属路由:本机痕迹的「通用流程」留在项目层,干净
|
|
|
1089
1089
|
assert.equal(evidencePath.chosenScope, 'project', '依据本身是本机文件不算普适证据');
|
|
1090
1090
|
assert.equal(evidencePath.reasonCode, 'machine_bound_procedure');
|
|
1091
1091
|
|
|
1092
|
+
const scopedProcedure = store.routeScope({
|
|
1093
|
+
title: 'npm 操作走 dsh-npm', summary: '本机 npm 命令用包装脚本',
|
|
1094
|
+
body: '本机 npm 一律用 dsh-npm;裸 npm 报 ENEEDAUTH 不是缺凭据',
|
|
1095
|
+
memoryClass: 'portable_procedure',
|
|
1096
|
+
conditions: [{ key: 'machine', value: 'macOS 桌面机' }],
|
|
1097
|
+
}, { project });
|
|
1098
|
+
assert.equal(scopedProcedure.chosenScope, 'global', '声明了机器条件的本机流程可进全局层');
|
|
1099
|
+
assert.equal(scopedProcedure.reasonCode, 'machine_scoped_procedure');
|
|
1100
|
+
|
|
1101
|
+
const scopedExplicit = store.routeScope({
|
|
1102
|
+
title: 'npm 操作走 dsh-npm', summary: '本机 npm 命令用包装脚本',
|
|
1103
|
+
body: '本机 npm 一律用 dsh-npm;裸 npm 报 ENEEDAUTH 不是缺凭据',
|
|
1104
|
+
memoryClass: 'portable_procedure', scope: 'global',
|
|
1105
|
+
conditions: [{ key: 'machine', value: 'macOS 桌面机' }],
|
|
1106
|
+
}, { project });
|
|
1107
|
+
assert.equal(scopedExplicit.chosenScope, 'global');
|
|
1108
|
+
assert.equal(scopedExplicit.reasonCode, 'explicit');
|
|
1109
|
+
|
|
1092
1110
|
const unconfirmed = store.routeScope({
|
|
1093
1111
|
title: '部署脚本', summary: '本机部署脚本', body: 'bash /tmp/deploy.sh',
|
|
1094
1112
|
memoryClass: 'portable_procedure', scope: 'global',
|