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.
@@ -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
- const RECALL_DEADLINE_MS = 150;
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 > RECALL_DEADLINE_MS) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-lite-memory",
3
- "version": "0.18.25",
3
+ "version": "0.18.26",
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",
package/store.js CHANGED
@@ -2520,7 +2520,8 @@ function machineBoundText(source) {
2520
2520
 
2521
2521
  /**
2522
2522
  * 适用范围判定:内容/依据的普适性优先,模型声明的 scope 只是提案。
2523
- * 模型说 global、但正文写死了本机路径或依据是本机文件时,先留在项目层(或问一次条件)。
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 (machineBound) return fallbackToProject('machine_bound_scope');
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 (machineBound) return fallbackToProject('machine_bound_preference');
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 (machineBound) return fallbackToProject('machine_bound_procedure');
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 });
@@ -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
- /* Windows reports the temp root as an 8.3 short name on one side and the long
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, fs.realpathSync(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, '归档正文');
@@ -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',