dsh-lite-memory 0.18.36 → 0.18.38
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/client.js +43 -1
- package/docs/BENCH.md +8 -8
- package/docs/RELEASE-0.18.37.md +8 -0
- package/docs/RELEASE-0.18.38.md +9 -0
- package/index.js +47 -36
- package/package.json +2 -2
- package/test/contract.test.mjs +25 -0
- package/test/evolution-read.test.mjs +19 -0
- package/test/recall.test.mjs +34 -0
- package/test/resident.test.mjs +46 -0
- package/view.js +2 -0
package/client.js
CHANGED
|
@@ -2516,6 +2516,45 @@ window.__ModuleLoader__.load({
|
|
|
2516
2516
|
});
|
|
2517
2517
|
};
|
|
2518
2518
|
|
|
2519
|
+
/** 跨层搬移是人的决定:模型只能提案,面板这一下才真的换层。 */
|
|
2520
|
+
const moveMemory = (record) => {
|
|
2521
|
+
if (busy || !record || !record.id) return;
|
|
2522
|
+
const target = selectedKey === 'global' ? 'project' : 'global';
|
|
2523
|
+
setBusy(true);
|
|
2524
|
+
setNote('');
|
|
2525
|
+
const operationId = newOperationId();
|
|
2526
|
+
let attempts = 0;
|
|
2527
|
+
const step = (resume) => {
|
|
2528
|
+
attempts += 1;
|
|
2529
|
+
const payload = resume ? { sessionId, operationId } : { sessionId, id: record.id, target, operationId };
|
|
2530
|
+
post(resume ? '/lite-memory/resume-move' : '/lite-memory/move', payload).then((result) => {
|
|
2531
|
+
if (!alive.current) return;
|
|
2532
|
+
if (result.status === 202) {
|
|
2533
|
+
if (attempts < 3) {
|
|
2534
|
+
step(true);
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
setBusy(false);
|
|
2538
|
+
setNote('搬移还没收尾,再点一次继续');
|
|
2539
|
+
load(selectedKey);
|
|
2540
|
+
return;
|
|
2541
|
+
}
|
|
2542
|
+
if (!alive.current || (selectedKey && scopeRef.current !== selectedKey)) return;
|
|
2543
|
+
setBusy(false);
|
|
2544
|
+
if (succeeded(result)) {
|
|
2545
|
+
setSelectedMemory('');
|
|
2546
|
+
setDetail(null);
|
|
2547
|
+
setEditingMemory(false);
|
|
2548
|
+
setNote(target === 'global' ? '已移到全局,各项目都能用' : '已移回当前项目');
|
|
2549
|
+
load(selectedKey);
|
|
2550
|
+
return;
|
|
2551
|
+
}
|
|
2552
|
+
setNote(failure(result, '没有移动'));
|
|
2553
|
+
});
|
|
2554
|
+
};
|
|
2555
|
+
step(false);
|
|
2556
|
+
};
|
|
2557
|
+
|
|
2519
2558
|
const setControlMode = (mode) => {
|
|
2520
2559
|
if (busy || !canToggle || mode === memoryMode) return;
|
|
2521
2560
|
setBusy(true);
|
|
@@ -2627,7 +2666,7 @@ window.__ModuleLoader__.load({
|
|
|
2627
2666
|
style: { display: 'flex', flexDirection: 'column', gap: '3px', padding: '7px 18px 8px', borderBottom: '1px solid var(--dsw-alias-border-l1)', flex: '0 0 auto' },
|
|
2628
2667
|
},
|
|
2629
2668
|
globalView
|
|
2630
|
-
? h('div', { style: { ...PANEL_MUTED, lineHeight: '20px' } }, '
|
|
2669
|
+
? h('div', { style: { ...PANEL_MUTED, lineHeight: '20px' } }, '全局记忆只读,可移回项目')
|
|
2631
2670
|
: h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(190px, 1fr))', gap: '4px 18px' } },
|
|
2632
2671
|
modeChoices(),
|
|
2633
2672
|
switchItem('自进化', evolutionOn ? '实验功能,会增加 token 消耗' : '从反馈里整理经验', evolutionOn, busy || !evolution || !canToggle || !memoryOn, toggleEvolution), // ui-copy-ignore 用户指定文案
|
|
@@ -2733,6 +2772,9 @@ window.__ModuleLoader__.load({
|
|
|
2733
2772
|
}, '修改'),
|
|
2734
2773
|
)
|
|
2735
2774
|
: null,
|
|
2775
|
+
!editingMemory && detailRecord && memoryOn && (globalView || canManage)
|
|
2776
|
+
? h(Action, { disabled: busy, onClick: () => moveMemory(detailRecord) }, selectedKey === 'global' ? '移回项目' : '移到全局')
|
|
2777
|
+
: null,
|
|
2736
2778
|
)
|
|
2737
2779
|
: h('div', { style: PANEL_MUTED }, detailNote || '读取中…'),
|
|
2738
2780
|
)
|
package/docs/BENCH.md
CHANGED
|
@@ -25,22 +25,22 @@ npm run bench:evolution # 实验规则/样例/检查点/回执的存储与投
|
|
|
25
25
|
|
|
26
26
|
## 固定数据集性能
|
|
27
27
|
|
|
28
|
-
2026-10-29,macOS arm64,Node v24.16.0,本地 SSD。数据:项目 200 条 + 全局 200 条,全部带标题、摘要、`when`、正文;另加一条正面场景,确认全局卡片真的进得来且不超预算。
|
|
28
|
+
2026-10-29,macOS arm64,Node v24.16.0,本地 SSD。数据:项目 200 条 + 全局 200 条,全部带标题、摘要、`when`、正文;另加一条正面场景,确认全局卡片真的进得来且不超预算。0.18.38 版含跨层搬移后的复测。
|
|
29
29
|
|
|
30
30
|
0.18.0 起每次保存都走「写临时文件 → fsync → rename → fsync 目录」并保留上一版,代价是单次保存多两次落盘;下面这组数是带这套持久化保证后测的。
|
|
31
31
|
|
|
32
32
|
| 指标 | 数值 |
|
|
33
33
|
| --- | --- |
|
|
34
|
-
| 准备时延 P50 |
|
|
35
|
-
| 准备时延 P95 |
|
|
36
|
-
| 冷启动首次准备 |
|
|
37
|
-
| 最慢一次 |
|
|
34
|
+
| 准备时延 P50 | 51.4 ms |
|
|
35
|
+
| 准备时延 P95 | 59.4 ms |
|
|
36
|
+
| 冷启动首次准备 | 76.5 ms |
|
|
37
|
+
| 最慢一次 | 76.5 ms |
|
|
38
38
|
| 每次准备上下文 | 368 B |
|
|
39
39
|
| 正面场景上下文 | 335 B,含 1 张全局卡片,未超 3 条 / 1200 字节 |
|
|
40
40
|
| 每次准备文件读取 | 0.2 |
|
|
41
41
|
| 本次数据规模 | 项目 200 条 + 全局 200 条 |
|
|
42
42
|
|
|
43
|
-
数据翻倍到两层各 200
|
|
43
|
+
数据翻倍到两层各 200 条后仍在 deadline 150 ms 内:0.18.38 复测 P95 59.4 ms。模型 provider 在准备阶段无 I/O、无网络。
|
|
44
44
|
|
|
45
45
|
## 长期序列
|
|
46
46
|
|
|
@@ -52,8 +52,8 @@ npm run bench:evolution # 实验规则/样例/检查点/回执的存储与投
|
|
|
52
52
|
| 全局活跃条目 | 200 |
|
|
53
53
|
| `state.json` | 396 KB(上限 2 MiB) |
|
|
54
54
|
| 层内文件 | `state.json`、`state.prev.json` 两个,不随轮数增长 |
|
|
55
|
-
| RSS |
|
|
56
|
-
| 序列耗时 |
|
|
55
|
+
| RSS | 219 MB(含进程内缓存与测试数据) |
|
|
56
|
+
| 序列耗时 | 5.8 s(150 轮全落盘) |
|
|
57
57
|
|
|
58
58
|
## 实验整理成本(0.11.0)
|
|
59
59
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# 0.18.37:npm 提醒先看当前项目层
|
|
2
|
+
|
|
3
|
+
- 动作前的 npm 提醒不再只查全局层:先查当前项目层的现行记忆,再查全局层,项目规则优先。
|
|
4
|
+
- 本机流程按记忆分层规矩留在项目层,也能在 `npm whoami` / `npm publish` 前拦一次,而不是因为没进全局层就失效。
|
|
5
|
+
- 同一条提醒仍然每轮最多一次,已交付过的条目不会再提示;项目记忆关闭时行为不变。
|
|
6
|
+
- 补回归:项目层与全局层各放一条 npm 规则,验证项目层优先且正文送达。
|
|
7
|
+
|
|
8
|
+
验证:`npm test` 全绿;`npm run bench` / `npm run bench:evolution` 均 exit 0;`style-lint` 通过。
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 0.18.38:面板可跨层搬移记忆
|
|
2
|
+
|
|
3
|
+
- 明细里新增搬移动作:项目记忆可移到全局,全局记忆可移回当前项目;全局视图只读,搬回由当前会话的项目层承接。
|
|
4
|
+
- 同一操作重复提交按 `operationId` 幂等;目标层有同名、近义或主题槽位冲突时拒绝并说明原因,不覆盖。
|
|
5
|
+
- 服务端只认会话身份:面板带 `sessionId`,项目目录由服务端查,客户端不用抄路径;未知会话回 `请求不完整`。
|
|
6
|
+
- 搬移后 id 换成新层身份,旧 id 留原层归档;出链在目标层不可见时拒绝,避免搬过去就断链。
|
|
7
|
+
- 补回归:保存探针、移到全局、再移回项目,校验层、id、正文与旧 id 归档读取。
|
|
8
|
+
|
|
9
|
+
验证:`npm test` 全绿;`npm run bench` exit 0(P50 51.4 ms / P95 59.4 ms,上下文 368 B);`style-lint` 通过。
|
package/index.js
CHANGED
|
@@ -739,7 +739,10 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
739
739
|
return sendJson(res, 200, { ok: true, ...listCitations(sessionId) });
|
|
740
740
|
}
|
|
741
741
|
|
|
742
|
-
|
|
742
|
+
/** 面板只带会话:工作目录由会话身份查,客户端不用抄路径。 */
|
|
743
|
+
const cwd = typeof body.cwd === 'string' && body.cwd
|
|
744
|
+
? body.cwd
|
|
745
|
+
: (typeof body.sessionId === 'string' ? cwdForSession(body.sessionId) || '' : '');
|
|
743
746
|
if (cwd.length === 0) return sendJson(res, 400, { ok: false, reason: '请求不完整' });
|
|
744
747
|
const scope = scopeOf(cwd);
|
|
745
748
|
const id = typeof body.id === 'string' ? body.id : '';
|
|
@@ -842,29 +845,29 @@ function refsOf(hits) {
|
|
|
842
845
|
const SAVE_PARAMS = {
|
|
843
846
|
type: 'object',
|
|
844
847
|
properties: {
|
|
845
|
-
updateTitle: { type: 'string', description: '
|
|
846
|
-
title: { type: 'string', description:
|
|
847
|
-
summary: { type: 'string', description:
|
|
848
|
-
body: { type: 'string', description: `正文 markdown
|
|
849
|
-
when: { type: 'string', description:
|
|
850
|
-
kind: { type: 'string', enum: ['preference', 'convention', 'procedure', 'pointer', 'pitfall', 'credential_hint', 'note'], description: '
|
|
851
|
-
tags: { type: 'array', items: { type: 'string' }, description: '最多 5
|
|
848
|
+
updateTitle: { type: 'string', description: '要更新的已有条目标题,原样抄搜索结果;同一件事更新时用,不新建' },
|
|
849
|
+
title: { type: 'string', description: `标题,名词短语,≤${LIMITS.titleChars} 字;就是条目身份` },
|
|
850
|
+
summary: { type: 'string', description: '一句话说明记的是什么,名词短语;超长自动截断' },
|
|
851
|
+
body: { type: 'string', description: `正文 markdown,≤${LIMITS.entryBytes} 字节;只写原则、判断规则、触发条件和边界` },
|
|
852
|
+
when: { type: 'string', description: `适用场景(哪台机器/什么环境/什么情况),≤${LIMITS.whenChars} 字;同标题不同 when 存两条;更新不给则保留` },
|
|
853
|
+
kind: { type: 'string', enum: ['preference', 'convention', 'procedure', 'pointer', 'pitfall', 'credential_hint', 'note'], description: '条目类型;更新不给则保留' },
|
|
854
|
+
tags: { type: 'array', items: { type: 'string' }, description: '最多 5 个标签;更新不给则保留' },
|
|
852
855
|
supersedes: {
|
|
853
856
|
type: 'string',
|
|
854
|
-
description: '
|
|
857
|
+
description: '被取代的旧条目标题,原样抄搜索结果;同层有效,旧条目保留在历史',
|
|
855
858
|
},
|
|
856
859
|
resolution: {
|
|
857
860
|
type: 'string',
|
|
858
861
|
enum: ['distinct'],
|
|
859
|
-
description: '
|
|
862
|
+
description: '确认「不是同一条」;仅当返回 needsResolution 且核对后确实不同时带上',
|
|
860
863
|
},
|
|
861
|
-
scope: { type: 'string', enum: ['auto', 'project', 'global'], description: '
|
|
862
|
-
memoryClass: { type: 'string', enum: ['personal_preference', 'project_fact', 'environment_fact', 'portable_procedure'], description: '
|
|
863
|
-
durability: { type: 'string', enum: ['durable', 'task'], description: '
|
|
864
|
-
evidenceRef: { type: 'string', description: '
|
|
864
|
+
scope: { type: 'string', enum: ['auto', 'project', 'global'], description: '存哪层:auto(默认,按类别判定)/ project / global。只有用户明确说「所有项目」才 global;本机路径、端口、项目事实会降为项目层;仅本机成立的流程声明 machine/runtime 条件后可进全局' },
|
|
865
|
+
memoryClass: { type: 'string', enum: ['personal_preference', 'project_fact', 'environment_fact', 'portable_procedure'], description: '归属提案:个人偏好/项目事实/设备事实/可搬流程;设备事实须带条件' },
|
|
866
|
+
durability: { type: 'string', enum: ['durable', 'task'], description: 'durable(默认)或 task(一次性参数,拒绝落盘)' },
|
|
867
|
+
evidenceRef: { type: 'string', description: '归属依据:哪条消息/文件/观察' },
|
|
865
868
|
sameTopicAs: {
|
|
866
869
|
type: 'object',
|
|
867
|
-
description: '
|
|
870
|
+
description: '绑定已有主题槽位;标题对不上则报错不写',
|
|
868
871
|
properties: {
|
|
869
872
|
scope: { type: 'string', enum: ['project', 'global'] },
|
|
870
873
|
title: { type: 'string' },
|
|
@@ -875,16 +878,16 @@ const SAVE_PARAMS = {
|
|
|
875
878
|
mode: {
|
|
876
879
|
type: 'string',
|
|
877
880
|
enum: ['patch', 'replace_current'],
|
|
878
|
-
description: '
|
|
881
|
+
description: 'patch(默认,未给字段保留)或 replace_current(整条替换,须显式给 title/summary/when/body/kind/tags)',
|
|
879
882
|
},
|
|
880
883
|
applicability: {
|
|
881
884
|
type: 'string',
|
|
882
885
|
enum: view.APPLICABILITY,
|
|
883
|
-
description: '
|
|
886
|
+
description: 'all_in_scope 普遍适用;conditional 须给 conditions;unresolved 还没定(默认)',
|
|
884
887
|
},
|
|
885
888
|
conditions: {
|
|
886
889
|
type: 'array',
|
|
887
|
-
description: '
|
|
890
|
+
description: '条件,最多 8 条;空数组表示清空',
|
|
888
891
|
items: {
|
|
889
892
|
type: 'object',
|
|
890
893
|
properties: {
|
|
@@ -897,23 +900,23 @@ const SAVE_PARAMS = {
|
|
|
897
900
|
},
|
|
898
901
|
selection: {
|
|
899
902
|
type: ['object', 'null'],
|
|
900
|
-
description: '
|
|
903
|
+
description: '只对某产物成立:complete 清单完整(须 artifactKey),examples 仅举例;null 清空',
|
|
901
904
|
properties: {
|
|
902
905
|
mode: { type: 'string', enum: view.SELECTION_MODES },
|
|
903
906
|
artifactKey: { type: 'string' },
|
|
904
|
-
items: { type: 'array', items: { type: 'string' }, description: 'examples
|
|
907
|
+
items: { type: 'array', items: { type: 'string' }, description: 'examples 的举例清单' },
|
|
905
908
|
},
|
|
906
909
|
required: ['mode'],
|
|
907
910
|
additionalProperties: false,
|
|
908
911
|
},
|
|
909
912
|
links: {
|
|
910
913
|
type: 'array',
|
|
911
|
-
description: '
|
|
914
|
+
description: '相关条目,最多 3 条;目标须存在;空数组清空',
|
|
912
915
|
items: {
|
|
913
916
|
type: 'object',
|
|
914
917
|
properties: {
|
|
915
918
|
type: { type: 'string', enum: view.LINK_TYPES, description: '默认 related' },
|
|
916
|
-
title: { type: 'string', description: '
|
|
919
|
+
title: { type: 'string', description: '被关联条目标题,原样抄搜索结果' },
|
|
917
920
|
},
|
|
918
921
|
required: ['title'],
|
|
919
922
|
additionalProperties: false,
|
|
@@ -922,9 +925,9 @@ const SAVE_PARAMS = {
|
|
|
922
925
|
triggers: {
|
|
923
926
|
type: 'array',
|
|
924
927
|
items: { type: 'string' },
|
|
925
|
-
description: '
|
|
928
|
+
description: '自动召回触发词,最多 4 个;空数组清空',
|
|
926
929
|
},
|
|
927
|
-
topicKey: { type: ['string', 'null'], description: '
|
|
930
|
+
topicKey: { type: ['string', 'null'], description: '主题槽位,去重用;null 清空' },
|
|
928
931
|
},
|
|
929
932
|
required: ['title', 'summary'],
|
|
930
933
|
additionalProperties: false,
|
|
@@ -932,7 +935,7 @@ const SAVE_PARAMS = {
|
|
|
932
935
|
|
|
933
936
|
const SUGGEST_PARAMS = {
|
|
934
937
|
...SAVE_PARAMS,
|
|
935
|
-
properties: Object.fromEntries(Object.entries(SAVE_PARAMS.properties).filter(([key]) => !['scope', 'id', 'updateTitle', 'resolution', 'memoryClass', 'durability', 'evidenceRef', 'sameTopicAs'].includes(key))),
|
|
938
|
+
properties: Object.fromEntries(Object.entries(SAVE_PARAMS.properties).filter(([key]) => !['scope', 'id', 'updateTitle', 'resolution', 'memoryClass', 'durability', 'evidenceRef', 'sameTopicAs', 'supersedes', 'mode'].includes(key))),
|
|
936
939
|
};
|
|
937
940
|
|
|
938
941
|
const SEARCH_PARAMS = {
|
|
@@ -940,9 +943,9 @@ const SEARCH_PARAMS = {
|
|
|
940
943
|
properties: {
|
|
941
944
|
query: { type: 'string', description: '要查的主题;用用户原话里的词,长句也行' },
|
|
942
945
|
limit: { type: 'integer', minimum: 1, maximum: 4, description: '最多返回几条,默认 3' },
|
|
943
|
-
history: { type: 'boolean', description: '
|
|
944
|
-
contextId: { type: 'string', description: '
|
|
945
|
-
bindForRequest: { type: 'boolean', description: '把这次选择绑定为本请求的任务;有 contextId
|
|
946
|
+
history: { type: 'boolean', description: '连已归档的旧记录一起找;默认只看当前有效' },
|
|
947
|
+
contextId: { type: 'string', description: '上一轮索引或工具结果给的任务上下文 id' },
|
|
948
|
+
bindForRequest: { type: 'boolean', description: '把这次选择绑定为本请求的任务;有 contextId 时带上' },
|
|
946
949
|
},
|
|
947
950
|
required: ['query'],
|
|
948
951
|
additionalProperties: false,
|
|
@@ -1605,11 +1608,11 @@ export function apply(ctx, config) {
|
|
|
1605
1608
|
continue;
|
|
1606
1609
|
}
|
|
1607
1610
|
/**
|
|
1608
|
-
*
|
|
1609
|
-
*
|
|
1611
|
+
* 只有任务范围条件(栏目/阶段/任务类型)才算相关度:环境条件(系统/机器/路径)人人都过,
|
|
1612
|
+
* 拿它当绑定条件会让一条宽泛规则出现在每个请求里。其余规则照旧过词面门。
|
|
1610
1613
|
*/
|
|
1611
1614
|
const conditions = Array.isArray(record.conditions) ? record.conditions.filter((row) => row && row.key) : [];
|
|
1612
|
-
const bound = conditions.
|
|
1615
|
+
const bound = conditions.some((row) => view.TASK_CONDITION_KEYS.includes(row.key));
|
|
1613
1616
|
if (!bound && score < view.MATCH_FLOOR) continue;
|
|
1614
1617
|
const topic = record.topicKey || record.id;
|
|
1615
1618
|
if (byTopic.has(topic) && byTopic.get(topic).body !== record.body) conflict = true;
|
|
@@ -1954,7 +1957,7 @@ export function apply(ctx, config) {
|
|
|
1954
1957
|
|
|
1955
1958
|
ctx.effect(() => tools.register({
|
|
1956
1959
|
name: 'memory_save',
|
|
1957
|
-
description:
|
|
1960
|
+
description: `保存一条跨会话有用的记忆。只在会改变以后判断时存;先搜,能更新就不新建。用户说「记住」或话已是长期规则(「以后都这样」「别再 X」)时直接调用,不要先问。文本相近但标题不同时先不写,返回 needsResolution 后核对:是同一件事就改成那个标题重存,确实是另一条才带 resolution="distinct"。按标题更新时未给字段保留,正文整段替换;旧正文只留作内部撤回,不展示不检索。前置条件不同同一标题可存两条。${NOTES}`,
|
|
1958
1961
|
parameters: SAVE_PARAMS,
|
|
1959
1962
|
output: {
|
|
1960
1963
|
presentationMeta: (_args, value) => ({
|
|
@@ -2176,7 +2179,7 @@ export function apply(ctx, config) {
|
|
|
2176
2179
|
|
|
2177
2180
|
ctx.effect(() => tools.register({
|
|
2178
2181
|
name: 'memory_suggest',
|
|
2179
|
-
description:
|
|
2182
|
+
description: `对用户没说、但跨会话有用的经验提出保存建议。调用后出现「保留」按钮,用户点了才进正式记忆;不要用问句征询。一次对话最多一条,48 小时内每项目一条;相近条目记为「更新候选」,保留后更新那条,不新建。${NOTES}`,
|
|
2180
2183
|
parameters: SUGGEST_PARAMS,
|
|
2181
2184
|
output: {
|
|
2182
2185
|
presentationMeta: (_args, value) => ({ kind: 'memory-suggest', id: value.id || '', asked: value.asked }),
|
|
@@ -2216,7 +2219,7 @@ export function apply(ctx, config) {
|
|
|
2216
2219
|
|
|
2217
2220
|
ctx.effect(() => tools.register({
|
|
2218
2221
|
name: 'memory_search',
|
|
2219
|
-
description:
|
|
2222
|
+
description: `检索已有记忆;命中后 memory_read 带标题读正文。用户提到「记住了什么/上次/规范/约定/路径/凭据怎么取」,或眼前反复出现报错、弹窗、异常时先查这里。跨项目条目以 [global] 开头。要更新就按返回标题重存。默认只列当前有效的;查历史传 history。`,
|
|
2220
2223
|
parameters: SEARCH_PARAMS,
|
|
2221
2224
|
output: {
|
|
2222
2225
|
presentationMeta: (_args, value) => ({
|
|
@@ -2671,8 +2674,16 @@ export function apply(ctx, config) {
|
|
|
2671
2674
|
const held = prepared.get(agent);
|
|
2672
2675
|
if (!held || held.npmHinted) return undefined;
|
|
2673
2676
|
if (!memoryOnForAgent(agent)) return undefined;
|
|
2674
|
-
|
|
2675
|
-
const
|
|
2677
|
+
/** 本机流程按规矩留在项目层;全局层是兜底,先看当前项目再看全局。 */
|
|
2678
|
+
const scopes = [];
|
|
2679
|
+
try {
|
|
2680
|
+
const project = scopeOf(cwdOf({ agent }));
|
|
2681
|
+
if (project && project.confirmed !== false && project.key !== 'global') scopes.push(project);
|
|
2682
|
+
} catch { /* 项目根还没确认时只看全局 */ }
|
|
2683
|
+
scopes.push(globalScope());
|
|
2684
|
+
const row = scopes.flatMap((scope) => {
|
|
2685
|
+
try { return loadIndex(scope).entries || []; } catch { return []; }
|
|
2686
|
+
}).find((entry) => {
|
|
2676
2687
|
if (!entry || entry.archived || entry.origin === 'project_evolution') return false;
|
|
2677
2688
|
/** 过期/被取代的规则不能再在动作前拦人。 */
|
|
2678
2689
|
if (view.lifecycleStatusOf(entry)) return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-lite-memory",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.38",
|
|
4
4
|
"description": "On-demand memory for the DeepSeek Harness: nothing resident, at most three small cards on a real hit, bodies read on demand.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"dsh": ">=0.1.5"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node test/store.test.mjs && node test/state.test.mjs && node test/view.test.mjs && node test/anchor.test.mjs && node test/recall.test.mjs && node test/evolution-read.test.mjs && node test/lexical.test.mjs && node test/layer-read.test.mjs && node test/control.test.mjs && node test/learning.test.mjs && node test/scenario.test.mjs && node test/crossing.test.mjs && node test/runtime-context.test.mjs && node test/client.test.mjs && node test/palace.test.mjs && node test/contract.test.mjs && node test/safety.test.mjs && node test/ha.test.mjs && node test/record.test.mjs && node test/eval-seed.test.mjs",
|
|
11
|
+
"test": "node test/store.test.mjs && node test/state.test.mjs && node test/view.test.mjs && node test/anchor.test.mjs && node test/recall.test.mjs && node test/evolution-read.test.mjs && node test/lexical.test.mjs && node test/layer-read.test.mjs && node test/control.test.mjs && node test/learning.test.mjs && node test/scenario.test.mjs && node test/crossing.test.mjs && node test/runtime-context.test.mjs && node test/resident.test.mjs && node test/client.test.mjs && node test/palace.test.mjs && node test/contract.test.mjs && node test/safety.test.mjs && node test/ha.test.mjs && node test/record.test.mjs && node test/eval-seed.test.mjs",
|
|
12
12
|
"bench": "node bench/bench.mjs",
|
|
13
13
|
"bench:evolution": "node bench/evolution.mjs"
|
|
14
14
|
},
|
package/test/contract.test.mjs
CHANGED
|
@@ -1299,4 +1299,29 @@ const cfgKeep = await call('/lite-memory/conflict', {
|
|
|
1299
1299
|
assert.equal(cfgKeep.status, 200);
|
|
1300
1300
|
assert.equal(store.conflictsOf(store.globalScope()).length, 0);
|
|
1301
1301
|
|
|
1302
|
+
// ── 面板搬移:只带会话就能换层,身份按 id 换但正文与条件跟着走 ─────────────
|
|
1303
|
+
const moveCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'lite-memory-contract-move-'));
|
|
1304
|
+
sessionCwds.set('session-move', moveCwd);
|
|
1305
|
+
store.saveEntry(store.scopeOf(moveCwd), {
|
|
1306
|
+
title: '搬移探针', summary: '只在项目层', body: '搬移正文。', tags: ['搬移'], writeMode: 'patch',
|
|
1307
|
+
});
|
|
1308
|
+
const movedOut = await call('/lite-memory/move', {
|
|
1309
|
+
sessionId: 'session-move', target: 'global',
|
|
1310
|
+
id: store.loadIndex(store.scopeOf(moveCwd)).entries.find((row) => row.title === '搬移探针').id,
|
|
1311
|
+
});
|
|
1312
|
+
assert.equal(movedOut.status, 200);
|
|
1313
|
+
assert.equal(movedOut.body.ok, true);
|
|
1314
|
+
assert.equal(movedOut.body.status, 'moved');
|
|
1315
|
+
assert.equal(movedOut.body.to, 'global');
|
|
1316
|
+
assert.ok(movedOut.body.id && movedOut.body.id !== movedOut.body.previousId, '换层后身份换新 id');
|
|
1317
|
+
assert.equal(store.loadIndex(store.scopeOf(moveCwd)).entries.some((row) => row.title === '搬移探针'), false, '项目层不再有这条');
|
|
1318
|
+
assert.equal(store.loadIndex(store.globalScope()).entries.some((row) => row.id === movedOut.body.id), true, '全局层拿到这条');
|
|
1319
|
+
const movedBack = await call('/lite-memory/move', {
|
|
1320
|
+
sessionId: 'session-move', target: 'project', id: movedOut.body.id,
|
|
1321
|
+
});
|
|
1322
|
+
assert.equal(movedBack.status, 200);
|
|
1323
|
+
assert.equal(movedBack.body.to, 'project');
|
|
1324
|
+
assert.equal(store.loadIndex(store.scopeOf(moveCwd)).entries.some((row) => row.id === movedBack.body.id), true, '搬回当前项目');
|
|
1325
|
+
assert.equal(store.loadIndex(store.globalScope()).entries.some((row) => row.id === movedOut.body.id), false, '全局层不再有这条');
|
|
1326
|
+
|
|
1302
1327
|
console.log('contract.test.mjs: all passed');
|
|
@@ -8,6 +8,7 @@ const { apply } = await import('../index.js');
|
|
|
8
8
|
const store = await import('../store.js');
|
|
9
9
|
const state = await import('../state.js');
|
|
10
10
|
const anchor = await import('../anchor.js');
|
|
11
|
+
const view = await import('../view.js');
|
|
11
12
|
|
|
12
13
|
const effect = (fn) => {
|
|
13
14
|
const dispose = fn();
|
|
@@ -637,3 +638,21 @@ console.log('evolution-read.test.mjs: all passed');
|
|
|
637
638
|
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('这期封面标题怎么写')));
|
|
638
639
|
assert.equal(runtime.text(agent, sig).includes('导出统一用 H.264'), false, '无关问题不注入宽泛规则');
|
|
639
640
|
}
|
|
641
|
+
|
|
642
|
+
/* L25:只声明环境条件(系统/路径)的规则不算任务绑定,问题不相关时不注入。 */
|
|
643
|
+
{
|
|
644
|
+
const dir = mkdir('env-only');
|
|
645
|
+
const scope = scopeOf(dir);
|
|
646
|
+
const host = view.hostTargetOs();
|
|
647
|
+
assert.ok(host, '本机系统可识别');
|
|
648
|
+
seedRule(scope, { title: '导出参数', body: '导出统一用 H.264。', conditions: [{ key: 'targetOs', value: host }] });
|
|
649
|
+
enable(scope, { seriesId: '口播' });
|
|
650
|
+
const runtime = boot();
|
|
651
|
+
const agent = makeAgent('a-env-only', dir);
|
|
652
|
+
const sig = signal();
|
|
653
|
+
await runtime.preStep({ agent, signal: sig, turn: 't1' }, enter(human('这期封面标题怎么写')));
|
|
654
|
+
assert.equal(runtime.text(agent, sig).includes('导出统一用 H.264'), false, '只有环境条件也要过词面门');
|
|
655
|
+
const sig2 = signal();
|
|
656
|
+
await runtime.preStep({ agent, signal: sig2, turn: 't2' }, enter(human('这期口播导出参数怎么配')));
|
|
657
|
+
assert.equal(runtime.text(agent, sig2).includes('导出统一用 H.264'), true, '相关时环境条件规则照常注入');
|
|
658
|
+
}
|
package/test/recall.test.mjs
CHANGED
|
@@ -846,6 +846,40 @@ console.log('recall.test.mjs: ok');
|
|
|
846
846
|
assert.ok(scopeNpm.key, '项目层不受影响');
|
|
847
847
|
}
|
|
848
848
|
|
|
849
|
+
// A27b:npm 提醒先看当前项目层,再看全局层;本机流程留在项目层也能在动作前拦人。
|
|
850
|
+
{
|
|
851
|
+
const cwdNpmProject = mkdir('npm-guard-project');
|
|
852
|
+
const scopeNpmProject = store.scopeOf(cwdNpmProject);
|
|
853
|
+
store.setRecallMode(scopeNpmProject, 'adaptive', { operationId: 'npm-guard-project-mode' });
|
|
854
|
+
assert.ok(store.upsert(global, {
|
|
855
|
+
title: 'npm 全局登录提醒',
|
|
856
|
+
summary: '全局层的 npm 登录说明',
|
|
857
|
+
body: '全局层:npm login 会误导,改用 dsh-npm。',
|
|
858
|
+
tags: ['npm'],
|
|
859
|
+
applicability: 'all_in_scope',
|
|
860
|
+
}).record);
|
|
861
|
+
assert.ok(store.upsert(scopeNpmProject, {
|
|
862
|
+
title: 'npm 项目发布前先看凭据',
|
|
863
|
+
summary: '这个项目的 npm 发布步骤',
|
|
864
|
+
body: '项目层:先读 Keychain 位置,再用 dsh-npm publish,不要 npm whoami。',
|
|
865
|
+
tags: ['npm', '发布'],
|
|
866
|
+
applicability: 'all_in_scope',
|
|
867
|
+
}).record);
|
|
868
|
+
const guards = [];
|
|
869
|
+
const agent = makeAgent('agent-npm-project', cwdNpmProject);
|
|
870
|
+
agent.ctx = {
|
|
871
|
+
tools: { guard: (fn) => { guards.push(fn); return () => {}; } },
|
|
872
|
+
effect: (fn) => fn(),
|
|
873
|
+
};
|
|
874
|
+
const runtime = boot();
|
|
875
|
+
runtime.handlers.get('agent/created').at(-1)({ agent });
|
|
876
|
+
const sig = signal();
|
|
877
|
+
await runtime.preStep({ agent, signal: sig, turn: 'npm-project-1' }, enter(human('先看下环境状态')));
|
|
878
|
+
const blocked = guards[0]({ name: 'bash', callId: 'call-npm-project', arguments: { command: 'npm publish' } });
|
|
879
|
+
assert.match(String(blocked), /项目发布前先看凭据/, '项目层规则优先于全局层');
|
|
880
|
+
assert.match(String(blocked), /Keychain/, '给项目层正文');
|
|
881
|
+
}
|
|
882
|
+
|
|
849
883
|
// A28:零命中的同一轮里,关键动作发生后补查一次;补到后不再重复注入。
|
|
850
884
|
{
|
|
851
885
|
const cwdActionKey = mkdir('action-key');
|
|
@@ -0,0 +1,46 @@
|
|
|
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-resident-'));
|
|
7
|
+
const { apply } = await import('../index.js');
|
|
8
|
+
|
|
9
|
+
const effect = (fn) => {
|
|
10
|
+
const dispose = fn();
|
|
11
|
+
return typeof dispose === 'function' ? dispose : () => {};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const defs = new Map();
|
|
15
|
+
const ctx = {
|
|
16
|
+
tools: { register: (def) => { defs.set(def.name, def); return () => {}; } },
|
|
17
|
+
effect,
|
|
18
|
+
inject: () => () => {},
|
|
19
|
+
get: () => undefined,
|
|
20
|
+
on: () => () => {},
|
|
21
|
+
};
|
|
22
|
+
apply(ctx);
|
|
23
|
+
|
|
24
|
+
const bytes = (value) => Buffer.byteLength(JSON.stringify(value ?? null));
|
|
25
|
+
const resident = (def) => bytes(def.description) + bytes(def.parameters) + bytes(def.output?.schema ?? null);
|
|
26
|
+
|
|
27
|
+
/* 常驻提示每轮都要发一遍:工具定义变大就是每次请求都变贵。 */
|
|
28
|
+
const caps = {
|
|
29
|
+
memory_save: 6500,
|
|
30
|
+
memory_suggest: 3900,
|
|
31
|
+
memory_search: 1800,
|
|
32
|
+
memory_read: 1400,
|
|
33
|
+
memory_forget: 1100,
|
|
34
|
+
};
|
|
35
|
+
let total = 0;
|
|
36
|
+
for (const [name, cap] of Object.entries(caps)) {
|
|
37
|
+
const def = defs.get(name);
|
|
38
|
+
assert.ok(def, `${name} registered`);
|
|
39
|
+
const size = resident(def);
|
|
40
|
+
total += size;
|
|
41
|
+
assert.ok(size <= cap, `${name} resident ${size}B exceeds ${cap}B`);
|
|
42
|
+
}
|
|
43
|
+
assert.ok(total <= 13600, `resident tool text ${total}B exceeds 13600B`);
|
|
44
|
+
assert.ok(defs.get('memory_suggest').parameters.properties.supersedes === undefined, 'suggest does not accept supersedes');
|
|
45
|
+
assert.ok(defs.get('memory_suggest').parameters.properties.mode === undefined, 'suggest does not accept mode');
|
|
46
|
+
console.log('resident tool text', total, 'bytes');
|
package/view.js
CHANGED
|
@@ -9,6 +9,8 @@ export const APPLICABILITY = ['all_in_scope', 'conditional', 'unresolved'];
|
|
|
9
9
|
export const SELECTION_MODES = ['complete', 'examples'];
|
|
10
10
|
export const LINK_TYPES = ['related'];
|
|
11
11
|
export const CONDITION_KEYS = ['targetOs', 'shell', 'runtime', 'machine', 'projectRoot', 'pathPrefix', 'artifactKey', 'taskKind', 'seriesId', 'phase'];
|
|
12
|
+
/** 真正把规则绑到「这一集任务」上的条件;其余条件只描述环境,不构成相关性。 */
|
|
13
|
+
export const TASK_CONDITION_KEYS = ['taskKind', 'seriesId', 'phase'];
|
|
12
14
|
|
|
13
15
|
/** 卡片与单次请求的硬上限:整张卡要么进来,要么省略,不截半句前提。 */
|
|
14
16
|
export const CARD = { max: 3, bytes: 1200 };
|