@max-null/dsh-memory 0.16.0 → 0.18.0

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/README.md CHANGED
@@ -116,7 +116,12 @@ md 文件是唯一事实源(`~/.dsh/prompt-library/*.md` 为 global;`<worksp
116
116
  ```
117
117
  模型 memory_save → status: approved,立即生效;命中密钥/凭据规则则隔离(不进检索也不进注入)
118
118
  给 injected 则同时钉住常驻(0.8.0),不受自动规则影响
119
+ 钉成常驻时返回 `resident`(total / injected / omitted / budget)——
120
+ 钉的那一刻就知道挤掉了几条,不必等下一轮的出局诊断行
119
121
  memory_search → 关键词/语义召回(只搜记忆;模板走 prompt_search 通道,不会混进候选池)
122
+ 默认最多返回 20 条,`limit: 0` 取消上限;条数等于 limit 时可能还有更多
123
+ 命中记账只记 BM25 前 5 名——`hitCount` 是"排前"的信号,不是"被检索到"的面;
124
+ 一次查询的真实命中数看 `query-log.json` 的 `hits` 字段
120
125
  人(面板 / 开关) → 钉住 / 删除 / 放行隔离记录 / 回滚
121
126
  memory_forget → 随时删除(删除始终是人的动作)
122
127
  ```
package/dist/index.js CHANGED
@@ -99,6 +99,38 @@ const HIT_SCHEMA = {
99
99
  score: { type: 'number', required: true },
100
100
  },
101
101
  };
102
+ /**
103
+ * `memory_save` / `memory_update` 的输出:记录本体 +(操作后仍是常驻时)预算占用快照。
104
+ *
105
+ * 单列一份而不往 {@link RECORD_SCHEMA} 里加:`memory_list` 一次返回全库,每条都带
106
+ * 一份占用快照既冗余,也与「这条记录本身」无关。预算快照只在**钉入/仍常驻**时出现,
107
+ * 因为那正是「挤掉别人」这个决定发生的那一刻——钉完再等下一轮的出局诊断行,人已经
108
+ * 想不起来是哪一次钉入造成的。
109
+ */
110
+ const SAVED_RECORD_SCHEMA = {
111
+ type: 'object',
112
+ additionalProperties: false,
113
+ properties: {
114
+ ...RECORD_SCHEMA.properties,
115
+ resident: {
116
+ type: 'object',
117
+ additionalProperties: false,
118
+ description: '钉入之后的常驻注入占用(仅当该记录为常驻时出现):total 候选条数、injected 本轮实际进场、omitted 被预算挤出、budget 字符预算。',
119
+ properties: {
120
+ total: { type: 'number', required: true },
121
+ injected: { type: 'number', required: true },
122
+ omitted: { type: 'number', required: true },
123
+ budget: { type: 'number' },
124
+ },
125
+ },
126
+ },
127
+ };
128
+ /**
129
+ * `memory_search` 的默认返回上限(0 = 不限)。宽查询会命中大半个库(本机 279 条库实测
130
+ * 命中面 35%–84%),返回体随库线性增长;20 条仍覆盖"有答案价值"的部分,20 名之后
131
+ * 分数已趋同(设计依据见 docs/设计/2026-10-21-memory-search-结果截断-设计方案.md)。
132
+ */
133
+ const DEFAULT_SEARCH_LIMIT = 20;
102
134
  /**
103
135
  * 体检报告的输出(0.12.0):四类清单一律只读。`summary` 与注入行同量级,直接念给人听
104
136
  * 不失真——这个工具的主要用法就是「你问起时我念给你看」。
@@ -278,6 +310,21 @@ const GUIDANCE = 'Use memory tools for cross-session preferences, habits, and pr
278
310
  + 'When the library grows, call memory_sweep for a read-only health check (stale records with reasons, '
279
311
  + 'long-unused ones, candidates that keep being retrieved without being pinned, and budget usage). '
280
312
  + '库变大之后用 memory_sweep 做只读体检——失效记录(带原因)、久未命中、被反复检索却未常驻的候选、以及预算占用。';
313
+ /**
314
+ * 钉入常驻后的预算占用快照(`memory_save` / `memory_update` 输出用)。刻意复用注入侧的
315
+ * `renderInjection`:两处各算一套的话,「工具里显示的占用」与「下一轮实际注入」会给出
316
+ * 不同的数字,而人恰恰是拿前者的数字去判断要不要钉的。
317
+ */
318
+ function residentUsage(memory, projectCwd, budget, summaryChars) {
319
+ const records = memory.recallRecords(projectCwd);
320
+ const rendered = renderInjection(records, budget, summaryChars);
321
+ return {
322
+ total: records.length,
323
+ injected: rendered.lines.length,
324
+ omitted: rendered.omitted,
325
+ ...rendered.budget === null ? {} : { budget: rendered.budget },
326
+ };
327
+ }
281
328
  // 0.5.1:`memory:recall` provider 可经 AssembleContext.agent 拿到当前会话
282
329
  // 的 header.cwd——注入 = global + 当前会话工作区的 approved+injected
283
330
  // (0.3.4 起工作区记忆就按会话 cwd 路由;provider 是同步回调,只读已
@@ -429,10 +476,11 @@ export async function apply(ctx, config) {
429
476
  },
430
477
  },
431
478
  output: {
432
- schema: RECORD_SCHEMA,
479
+ schema: SAVED_RECORD_SCHEMA,
433
480
  render: (_args, value) => renderJson(value),
434
481
  },
435
482
  execute(args, exec) {
483
+ const cwd = execProjectCwd(exec);
436
484
  return memory.remember({
437
485
  content: args.content,
438
486
  ...args.namespace === undefined ? {} : { namespace: args.namespace },
@@ -442,7 +490,14 @@ export async function apply(ctx, config) {
442
490
  ...args.anchor === undefined
443
491
  ? {}
444
492
  : { anchor: args.anchor },
445
- }, execProjectCwd(exec)).then(recordValue);
493
+ }, cwd).then(record => {
494
+ const value = recordValue(record);
495
+ // 钉常驻的那一刻就把「挤掉了几条」交给人看:等到下一轮的出局诊断行,
496
+ // 人已经想不起来是哪一次钉入造成的。
497
+ return record.injected
498
+ ? { ...value, resident: residentUsage(memory, cwd, injectionBudget, summaryChars) }
499
+ : value;
500
+ });
446
501
  },
447
502
  presentCall: args => present('Save memory', 'other', args.content),
448
503
  }));
@@ -476,23 +531,28 @@ export async function apply(ctx, config) {
476
531
  }));
477
532
  ctx.tools.register(defineTool({
478
533
  name: registered('memory_search'),
479
- description: 'Recall stored memories by keyword. Deterministic literal matching — a miss means no stored term matched the query. 按关键词检索记忆(中文 2-gram + 关键词加权;配置嵌入时含语义融合;含待审核条目,被隔离的除外)。',
534
+ description: 'Recall stored memories by keyword. Deterministic literal matching — a miss means no stored term matched the query. Returns at most `limit` hits (default 20; 0 = no limit) — when the count equals limit there may be more. Narrow multi-word queries rank far better than one common token. 按关键词检索记忆(中文 2-gram + 关键词加权;配置嵌入时含语义融合;含待审核条目,被隔离的除外)。最多返回 limit 条(默认 20;0 = 不限),条数等于 limit 时可能还有更多;多词窄查询比单个常用词排序好得多。',
480
535
  parameters: {
481
536
  query: { type: 'string', required: true, description: 'Keyword query. 关键词查询.' },
482
537
  namespace: { type: 'string', enum: ['global', 'project'], description: 'Restrict to one namespace. 限定单个命名空间.' },
483
538
  status: { type: 'string', enum: ['suggested', 'approved'], description: 'Restrict to one review status. 限定审核状态.' },
484
539
  kind: { type: 'string', enum: ['fact', 'prompt'], description: 'Restrict by record kind — fact (memories, default) or prompt (templates; use prompt_search instead). 按记录类别过滤:fact=记忆(默认)/ prompt=模板(模板请用 prompt_search).' },
540
+ limit: { type: 'integer', default: DEFAULT_SEARCH_LIMIT, description: 'Maximum hits to return (default 20; 0 = no limit; negative falls back to 20). 最多返回条数(默认 20;0 = 不限;负数按 20).' },
485
541
  },
486
542
  output: {
487
543
  schema: { type: 'array', items: HIT_SCHEMA },
488
544
  render: (_args, value) => renderJson(value),
489
545
  },
490
546
  execute(args, exec) {
547
+ // 截断只在工具层:`memory.search()` 必须保持「全部正分命中」的语义,
548
+ // prompt_search 依赖它的全量结果给模板排序(本文件下方 prompt_search 的 hitIds)。
549
+ const requested = args.limit ?? DEFAULT_SEARCH_LIMIT;
550
+ const limit = requested < 0 ? DEFAULT_SEARCH_LIMIT : requested;
491
551
  return memory.search(args.query, {
492
552
  ...args.namespace === undefined ? {} : { namespace: args.namespace },
493
553
  ...args.status === undefined ? {} : { status: args.status },
494
554
  ...args.kind === undefined ? {} : { kind: args.kind },
495
- }, execProjectCwd(exec)).then(hits => hits.map(hitValue));
555
+ }, execProjectCwd(exec)).then(hits => (limit === 0 ? hits : hits.slice(0, limit)).map(hitValue));
496
556
  },
497
557
  presentCall: args => present('Search memory', 'read', args.query),
498
558
  }));
@@ -693,18 +753,25 @@ export async function apply(ctx, config) {
693
753
  },
694
754
  },
695
755
  output: {
696
- schema: RECORD_SCHEMA,
756
+ schema: SAVED_RECORD_SCHEMA,
697
757
  render: (_args, value) => renderJson(value),
698
758
  },
699
759
  execute(args, exec) {
700
760
  const cwd = execProjectCwd(exec);
701
761
  const id = args.id;
762
+ const settle = (record) => {
763
+ const value = recordValue(record);
764
+ // 改内容也会改变注入占用的字符数:只要这条仍是常驻,就把占用一起交回去。
765
+ return record.injected
766
+ ? { ...value, resident: residentUsage(memory, cwd, injectionBudget, summaryChars) }
767
+ : value;
768
+ };
702
769
  // 三种动作互斥,按 retract > supersedes > update 的次序取第一个(与工具描述一致):
703
770
  // 前两个是「标状态」,第三个才是「改内容」。同时给多个时以更重的动作为准。
704
771
  if (args.retract !== undefined)
705
- return memory.retract(id, args.retract, cwd).then(recordValue);
772
+ return memory.retract(id, args.retract, cwd).then(settle);
706
773
  if (args.supersedes !== undefined)
707
- return memory.supersede(id, args.supersedes, cwd).then(recordValue);
774
+ return memory.supersede(id, args.supersedes, cwd).then(settle);
708
775
  return memory.update(id, {
709
776
  ...args.content === undefined ? {} : { content: args.content },
710
777
  ...args.keywords === undefined ? {} : { keywords: args.keywords },
@@ -713,7 +780,7 @@ export async function apply(ctx, config) {
713
780
  ...args.anchor === undefined
714
781
  ? {}
715
782
  : { anchor: args.anchor },
716
- }, cwd).then(recordValue);
783
+ }, cwd).then(settle);
717
784
  },
718
785
  presentCall: args => present('Update memory', 'other', args.id),
719
786
  }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max-null/dsh-memory",
3
- "version": "0.16.0",
3
+ "version": "0.18.0",
4
4
  "description": "Cross-session plaintext memory plugin for the DeepSeek Harness — deterministic BM25 recall, writes take effect immediately, signal-driven injection, no embeddings",
5
5
  "license": "MIT",
6
6
  "publishConfig": {