@max-null/dsh-memory 0.9.0 → 0.9.2

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
@@ -41,7 +41,7 @@ npm install @max-null/dsh-memory
41
41
 
42
42
  - **服务** `ctx.memory`:`remember` / `list` / `search` / `forget` / `setStatus`
43
43
  - **工具**:`memory_save`、`memory_list`、`memory_search`、`memory_confirm`、`memory_forget`、`memory_update`
44
- - **注入**:`tool:memory` 指引 section + `memory:recall` 召回 context(global 的 `approved + injected` + 当前会话工作区的 `approved + injected`,带 `[memory:<id>:<namespace>]` 来源标记;摘要化 + 预算截断)
44
+ - **注入**:`tool:memory` 指引 section(工具用法 + 常驻注入判据)+ `memory:self` 机制自述 + `memory:recall` 召回 context(global 的 `approved + injected` + 当前会话工作区的 `approved + injected`,带 `[memory:<id>:<namespace>]` 来源标记;摘要化 + 预算截断)
45
45
  - **检索**:BM25(CJK 单字 + 2-gram,content 与 keywords 字段分离加权;中文多字查询精度显著优于单字切分);可选语义融合(见「可选配置」)
46
46
 
47
47
  ## 两层存储(global / project)
@@ -86,7 +86,7 @@ memory_forget → 随时删除(删除始终是人的动作)
86
86
 
87
87
  自动规则对**低频但关键**的记忆(长期约定、判据、委托)够不着:它们不会被反复检索,够不到阈值;勉强够到也会被 30 天撤下。这类走显式路径——在 `memory_save` / `memory_update` 里给 `injected` 即钉住,写的是与面板开关同一套语义(`injectedAuto: false`),此后不受两条自动规则影响。未审核与已隔离的记录不接受注入。
88
88
 
89
- 注入有**字符预算**(默认 1500,config `injectionBudget`):装不下的条目**整条丢弃**(不截内容),按「最近更新优先」取舍——所以 `injected: true` 不等于「每轮真的在场」,它只保证有资格排队。0.9.0 起这种出局不再无声:注入末尾会追加一行 `(另有 N 条常驻因预算未注入:id1, id2, …)`,**清理干净即自行消失**;面板的注入预览里也能展开看明细。诊断行本身不占预算。
89
+ 注入有**字符预算**(默认 1500,config `injectionBudget`):装不下的条目**整条丢弃**(不截内容),按「最近更新优先」取舍——所以 `injected: true` 不等于「每轮真的在场」,它只保证有资格排队。0.9.0 起这种出局不再无声:注入末尾会追加一行 `(另有 N 条常驻因预算未注入:…)`,0.9.1 起列出的是**每条的短摘要**而不是 id——查 id 是什么的那一步最容易省略,省略了就等于没报;**清理干净即自行消失**。面板的注入预览里也能展开看明细。诊断行本身不占预算。
90
90
 
91
91
  记忆还可声明**有效性锚点**(环境变量 / 工具清单 / 插件版本):所绑的值变化后,这条记忆被标记 `stale` 并撤下常驻,检索结果里也带出失效提示——环境变了,旧结论就不再被当成仍然正确。
92
92
 
package/dist/index.js CHANGED
@@ -92,15 +92,22 @@ function promptValue(record) {
92
92
  }
93
93
  // 0.3.0:审核语义双语(host 侧 GUIDANCE 无法跟随 DSH locale 动态切换,
94
94
  // 采用中英双语都写、模型自取——设计文档「风险与注意」)。
95
+ // 0.9.2:常驻注入不再是「人工开关」——0.8.0 起模型侧可经 `injected` 参数显式钉住。
96
+ // 旧表述会让模型以为自己没有该权限,也与同段「人工只在例外时介入」自相矛盾。
95
97
  const GUIDANCE = 'Use memory tools for cross-session preferences, habits, and project conventions. '
96
98
  + '记忆工具用于跨会话的偏好、习惯与项目约定。'
97
99
  + 'memory_save writes a memory that takes effect immediately (`approved`) — memories are maintained '
98
100
  + 'by the system and the model, and humans intervene only as exceptions. Credential-like content is '
99
101
  + 'quarantined instead. memory_save 写入即生效(`approved`)——记忆由系统与模型自行维护,人工只在例外时介入;'
100
102
  + '命中危险内容规则(密钥/凭据)的写入会被隔离。'
101
- + 'Approval only marks the content reviewed; whether it is injected every turn is a separate '
102
- + 'human-controlled switch (`injected`). 审核通过只代表内容被认可;是否每轮常驻注入由独立的'
103
- + '人工开关(`injected`)控制。'
103
+ + 'Pass `injected: true` on memory_save / memory_update to pin a memory resident in every turn\'s context: '
104
+ + 'the same switch the settings panel exposes, exempting the record from both automatic rules (promote on '
105
+ + 'repeated hits, demote after long idle). Reserve it for rules, standing agreements and judgement criteria '
106
+ + '— the kind that must apply even when nobody thinks to search for them; facts, references and case notes '
107
+ + 'stay search-only. '
108
+ + '给 memory_save / memory_update 传 `injected: true` 即把这条钉成每轮常驻——与设置面板是同一个开关,'
109
+ + '此后不受两条自动规则(反复命中自动开启 / 长期未命中自动撤下)影响。只用于规则、长期约定与判据这类'
110
+ + '「没人想起来搜也必须生效」的记忆;事实、参考与案例留作按需检索。'
104
111
  + 'When earlier context may be relevant, call memory_search to recall it — reviewable memories '
105
112
  + '相关历史上下文可用 memory_search 检索;被隔离的记录不进检索。'
106
113
  + 'Every memory is plaintext and inspectable with memory_list; memory_forget removes one. '
@@ -9,8 +9,10 @@ import type { MemoryRecord } from './engine.ts';
9
9
  export declare const DEFAULT_SUMMARY_CHARS = 80;
10
10
  /** 注入预算(字符):0.5.2 起默认开启,防常驻注入随记忆增长膨胀上下文。 */
11
11
  export declare const DEFAULT_INJECTION_BUDGET = 1500;
12
- /** 诊断行列出的 id 上限(2026-09-18):超出只列前 N 个,防这行本身失控。 */
13
- export declare const OMITTED_ID_LIMIT = 12;
12
+ /** 诊断行列出的条数上限(2026-09-18):超出只列前 N 个,防这行本身失控。 */
13
+ export declare const OMITTED_NOTICE_LIMIT = 12;
14
+ /** 诊断行里每条出局者的摘要长度(2026-09-18):够认出是哪条即可。 */
15
+ export declare const OMITTED_SUMMARY_CHARS = 24;
14
16
  /**
15
17
  * 确定性摘要:取首行,超过 maxChars 截断加省略号。
16
18
  * 空内容/空首行回退为去掉换行后的前 maxChars(保证 any 内容有摘要)。
@@ -26,13 +28,14 @@ export interface InjectionRender {
26
28
  /** 因预算被省略的候选条数(0 = 全部注入)。 */
27
29
  omitted: number;
28
30
  /**
29
- * 被省略条目的明细:`id` 供预算诊断行与后续清理定位,`line` 与 {@link lines}
30
- * 同格式,供面板明细直接渲染。诊断行**不占预算**——它由 `recallText` 追加在
31
- * 注入行之后,不参与这里的截断。
31
+ * 被省略条目的明细:`summary` 供预算诊断行报出「是哪条」,`line` 与
32
+ * {@link lines} 同格式、供面板明细直接渲染,`id` 备用。诊断行**不占预算**——
33
+ * 它由 `recallText` 追加在注入行之后,不参与这里的截断。
32
34
  */
33
35
  omittedRecords: Array<{
34
36
  id: string;
35
37
  line: string;
38
+ summary: string;
36
39
  }>;
37
40
  /** 实际注入的字符数(摘要 + 标记开销之和;不含 {@link omittedRecords})。 */
38
41
  chars: number;
@@ -44,8 +47,10 @@ export interface InjectionRender {
44
47
  */
45
48
  export declare function renderInjection(records: readonly MemoryRecord[], budget: number | null, summaryChars: number): InjectionRender;
46
49
  /**
47
- * 预算诊断行(2026-09-18):列 id 而非摘要——列全文要约 4000 字符,id 列表约
48
- * 100 字符。**不占注入预算**(调用方把它追加在注入行之后),清理干净后自行消失
49
- * (`omitted === 0` 返回空串);超出 {@link OMITTED_ID_LIMIT} 只列前 N 个。
50
+ * 预算诊断行(2026-09-18):列出被预算挡在外面的条目,**带短摘要而不是只有 id**——
51
+ * 「主动去查这个 id 是什么」正是最容易省略的那一步,而省略它就等于看不出来。
52
+ * 只报出局者、不报进场者(后者本来就在注入列表里)。**不占注入预算**(调用方把它
53
+ * 追加在注入行之后),清理干净后自行消失(`omitted === 0` 返回空串);超出
54
+ * {@link OMITTED_NOTICE_LIMIT} 只列前 N 个。
50
55
  */
51
56
  export declare function omittedNotice(rendered: InjectionRender): string;
package/dist/injection.js CHANGED
@@ -8,8 +8,10 @@
8
8
  export const DEFAULT_SUMMARY_CHARS = 80;
9
9
  /** 注入预算(字符):0.5.2 起默认开启,防常驻注入随记忆增长膨胀上下文。 */
10
10
  export const DEFAULT_INJECTION_BUDGET = 1500;
11
- /** 诊断行列出的 id 上限(2026-09-18):超出只列前 N 个,防这行本身失控。 */
12
- export const OMITTED_ID_LIMIT = 12;
11
+ /** 诊断行列出的条数上限(2026-09-18):超出只列前 N 个,防这行本身失控。 */
12
+ export const OMITTED_NOTICE_LIMIT = 12;
13
+ /** 诊断行里每条出局者的摘要长度(2026-09-18):够认出是哪条即可。 */
14
+ export const OMITTED_SUMMARY_CHARS = 24;
13
15
  /** 单条注入行的固定标记开销(不含摘要本体)。 */
14
16
  const LINE_OVERHEAD = 27;
15
17
  /**
@@ -46,7 +48,11 @@ export function renderInjection(records, budget, summaryChars) {
46
48
  const cost = text.length + LINE_OVERHEAD;
47
49
  if (budget !== null && budget > 0 && chars + cost > budget) {
48
50
  omitted++;
49
- omittedRecords.push({ id: String(record.id), line: text });
51
+ omittedRecords.push({
52
+ id: String(record.id),
53
+ line: text,
54
+ summary: deriveSummary(record.content, OMITTED_SUMMARY_CHARS),
55
+ });
50
56
  continue;
51
57
  }
52
58
  lines.push(text);
@@ -55,15 +61,17 @@ export function renderInjection(records, budget, summaryChars) {
55
61
  return { lines, budget: budget ?? null, omitted, omittedRecords, chars };
56
62
  }
57
63
  /**
58
- * 预算诊断行(2026-09-18):列 id 而非摘要——列全文要约 4000 字符,id 列表约
59
- * 100 字符。**不占注入预算**(调用方把它追加在注入行之后),清理干净后自行消失
60
- * (`omitted === 0` 返回空串);超出 {@link OMITTED_ID_LIMIT} 只列前 N 个。
64
+ * 预算诊断行(2026-09-18):列出被预算挡在外面的条目,**带短摘要而不是只有 id**——
65
+ * 「主动去查这个 id 是什么」正是最容易省略的那一步,而省略它就等于看不出来。
66
+ * 只报出局者、不报进场者(后者本来就在注入列表里)。**不占注入预算**(调用方把它
67
+ * 追加在注入行之后),清理干净后自行消失(`omitted === 0` 返回空串);超出
68
+ * {@link OMITTED_NOTICE_LIMIT} 只列前 N 个。
61
69
  */
62
70
  export function omittedNotice(rendered) {
63
71
  if (rendered.omitted === 0)
64
72
  return '';
65
- const ids = rendered.omittedRecords.map(record => String(record.id).slice(0, 8));
66
- const shown = ids.slice(0, OMITTED_ID_LIMIT);
67
- const truncated = ids.length > shown.length ? '…' : '';
68
- return `(另有 ${rendered.omitted} 条常驻因预算未注入:${shown.join(', ')}${truncated})`;
73
+ const shown = rendered.omittedRecords.slice(0, OMITTED_NOTICE_LIMIT);
74
+ const labels = shown.map(record => record.summary);
75
+ const truncated = rendered.omittedRecords.length > shown.length ? '…' : '';
76
+ return `(另有 ${rendered.omitted} 条常驻因预算未注入:${labels.join('、')}${truncated})`;
69
77
  }
package/dist/self.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * 记忆机制自述(memory:self 注入内容 + 面板「注入预览」共用)。
3
3
  * 0.3.2 起:LLM 每轮知道本环境有记忆机制;不落用户存储,随发版更新,
4
4
  * 版本号动态读取 package.json("只跟随 dsh-memory 组件发版变动")。
5
+ * 0.9.2:补齐常驻注入机制——`injected` 参数、注入预算上限、预算诊断行的含义。
5
6
  */
6
7
  import { createRequire } from 'node:module';
7
8
  export const SELF_VERSION = (() => {
@@ -25,6 +26,10 @@ export const SELF_DESCRIPTION = `[记忆系统自述] 本环境内置跨会话
25
26
  + '**怎么写**:一条一件事,并给多角度关键词(同义词、缩写、中英),否则将来检索不到它。'
26
27
  + '**锚点(可选)**:若这条记忆描述的是「随某个环境值变化的事实」(某工具版本下的行为、某环境变量决定的配置),用 anchor 参数把它绑到那个值上——值变了它会自动失效并标 stale,不会再被当成仍然正确。'
27
28
  + '写入即生效(`approved`),人工只在例外时介入;命中密钥/凭据规则的写入会被隔离(不进注入与检索)。'
28
- + '每轮注入的是「global + 当前会话工作区」里已生效且开着常驻开关的记忆摘要(受预算限制,超预算按最近更新优先);全文要靠 memory_search。'
29
+ + '**常驻注入**:每轮注入「global + 当前会话工作区」里 approved 且开着常驻开关的记忆单行摘要(形如 `- [memory:id:namespace] 摘要`);全文要靠 memory_search 取。'
30
+ + '开关有两条自动规则:命中累计 2 次自动开启、30 天未命中自动撤下;**给 memory_save / memory_update 传 `injected` 即显式接管**,此后这两条都不再适用。'
31
+ + '**钉的判据是「失效可察觉性」**:这条记忆若没被想起来,你会不会根本意识不到自己漏了它?会 → 钉常驻(规则 / 委托 / 判据);不会 → 留检索(事实 / 参考 / 案例)。'
32
+ + '**注入有预算**(默认 1500 字符,约装 11 条),装不下的整条丢弃、按最近使用优先取舍——所以「钉了常驻」只等于「有资格排队」。'
33
+ + '装不下时注入末尾会出现一行 `(另有 N 条常驻因预算未注入:…短摘要…)`——**那是「有常驻记忆并不在场」的唯一信号**,看到它就该逐条评估撤下。'
29
34
  + '发现某条记忆与实际不符时用 memory_update 修正(保持生效);确认没用的用 memory_forget。'
30
- + '(English: dsh-memory provides cross-session memory. Writes take effect immediately; save durable preferences, corrections and reusable conclusions — not transient state, guesses, or facts readable from the repo (record where to look instead). Always add multi-angle keywords.)';
35
+ + '(English: dsh-memory provides cross-session memory. Writes take effect immediately; save durable preferences, corrections and reusable conclusions — not transient state, guesses, or facts readable from the repo (record where to look instead). Always add multi-angle keywords. Pass `injected: true` to keep one resident every turn — reserve that for rules and standing agreements; facts stay search-only. Resident injection runs on a budget, so a pinned memory is only queued, never guaranteed to be in context.)';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max-null/dsh-memory",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
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": {