@max-null/dsh-memory 0.12.0 → 0.12.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/dist/engine.d.ts CHANGED
@@ -132,6 +132,14 @@ export interface MemoryPatch {
132
132
  * `injectedAuto: false`,此后不受自动升降影响);省略则保持原值。
133
133
  */
134
134
  injected?: boolean;
135
+ /**
136
+ * 有效性锚点(0.12.1):省略保持原值,给值改写,给 `null` 清除。
137
+ *
138
+ * 给值时**当场校验一次**并同步 `stale`(成立即复活、不成立即标失效、探测不到则不动)——
139
+ * 改写锚点的意图就是「让它重新被判定」,等到下次会话维护再判定会留下一个说不清的空窗
140
+ * (改了锚点却仍显示失效)。清除锚点时连「因该锚点而标的失效」一起清。
141
+ */
142
+ anchor?: MemoryAnchor | null;
135
143
  }
136
144
  export interface MemoryHit {
137
145
  record: MemoryRecord;
@@ -303,6 +311,9 @@ export declare class MemoryEngine extends Service {
303
311
  /**
304
312
  * 最近一次体检结果(只读)。未跑过时为 `undefined`——注入侧据此决定要不要报那一行,
305
313
  * 它只在真有新失效时出现,稀有所以不构成噪音。
314
+ *
315
+ * **每次 `sweep()`(含工具调用)都刷新它**(0.12.1)。此前只在会话启动维护里赋值,于是
316
+ * 那句提示在整个会话内不再变化,可能一直挂着早已被删掉的记录。
306
317
  */
307
318
  get lastSweepReport(): SweepReport | undefined;
308
319
  constructor(ctx: import('@deepseek-ai/cordis').Context, config?: MemoryConfig);
@@ -574,6 +585,13 @@ export declare class MemoryEngine extends Service {
574
585
  * @returns 本次状态发生变化的条数。
575
586
  */
576
587
  verifyAnchors(probes: AnchorProbes, projectCwd?: string): Promise<number>;
588
+ /**
589
+ * 锚点失效的原因文案:`verifyAnchorsInner` 与 `update` 的锚点改写共用一处。
590
+ *
591
+ * 共用不是为省几行——「改写或清除锚点时顺带清失效」要认得出「这条失效是本锚点标的」,
592
+ * 两处生成的文案必须逐字相同;认错就会把撤回、被取代标的失效一起抹掉。
593
+ */
594
+ private static anchorStaleReason;
577
595
  /** {@link verifyAnchors} 的实现体;必须经 `writeScope` 进入。 */
578
596
  private verifyAnchorsInner;
579
597
  /**
package/dist/engine.js CHANGED
@@ -204,6 +204,9 @@ export class MemoryEngine extends Service {
204
204
  /**
205
205
  * 最近一次体检结果(只读)。未跑过时为 `undefined`——注入侧据此决定要不要报那一行,
206
206
  * 它只在真有新失效时出现,稀有所以不构成噪音。
207
+ *
208
+ * **每次 `sweep()`(含工具调用)都刷新它**(0.12.1)。此前只在会话启动维护里赋值,于是
209
+ * 那句提示在整个会话内不再变化,可能一直挂着早已被删掉的记录。
207
210
  */
208
211
  get lastSweepReport() {
209
212
  return this.lastSweep;
@@ -546,7 +549,7 @@ export class MemoryEngine extends Service {
546
549
  .map(record => ({ id: String(record.id), summary: brief(record), hitCount: record.hitCount ?? 0 }));
547
550
  const resident = this.recallRecords(projectCwd);
548
551
  const rendered = renderInjection(resident, this.config.injectionBudget ?? DEFAULT_INJECTION_BUDGET, summaryChars);
549
- return {
552
+ const report = {
550
553
  stale,
551
554
  longUnused,
552
555
  candidates,
@@ -559,6 +562,11 @@ export class MemoryEngine extends Service {
559
562
  omitted: rendered.omitted,
560
563
  },
561
564
  };
565
+ // 每次体检都写回最近一次结果(0.12.1):注入侧那句「本次校验发现 N 条失效」读的就是它。
566
+ // 原先只有会话启动维护会赋值,于是调用 `memory_sweep` 刷新不了它——报告里挂着早已被删掉
567
+ // 的记录,整个会话内不再变化。(这条只写实例字段,不影响「只读」的承诺:不落盘、不改记忆。)
568
+ this.lastSweep = report;
569
+ return report;
562
570
  }
563
571
  /**
564
572
  * 候选(0.12.0):被反复检索命中、但没有被钉成常驻的记录。**只提示,不改状态。**
@@ -844,7 +852,9 @@ export class MemoryEngine extends Service {
844
852
  this.ctx.logger?.info?.(`dsh-memory: 锚点校验更新 ${staled} 条`);
845
853
  // 体检(0.12.0):只读,产物存进实例字段,供注入侧报「本次校验发现的新失效」。
846
854
  // 它在 try 内——报告没有记忆本身重要,失败由下面的 catch 兜住、不阻断启动。
847
- this.lastSweep = await this.sweep(projectCwd);
855
+ // 写回由 `sweep()` 自己做(0.12.1),这里不再赋值——于是手工调用 `memory_sweep`
856
+ // 也能刷新注入里那句提示。
857
+ await this.sweep(projectCwd);
848
858
  }
849
859
  catch (error) {
850
860
  this.ctx.logger?.warn?.(`dsh-memory: 启动维护失败(不影响使用):${String(error)}`);
@@ -1083,6 +1093,16 @@ export class MemoryEngine extends Service {
1083
1093
  async verifyAnchors(probes, projectCwd) {
1084
1094
  return this.writeScope(projectCwd, () => this.verifyAnchorsInner(probes, projectCwd));
1085
1095
  }
1096
+ /**
1097
+ * 锚点失效的原因文案:`verifyAnchorsInner` 与 `update` 的锚点改写共用一处。
1098
+ *
1099
+ * 共用不是为省几行——「改写或清除锚点时顺带清失效」要认得出「这条失效是本锚点标的」,
1100
+ * 两处生成的文案必须逐字相同;认错就会把撤回、被取代标的失效一起抹掉。
1101
+ */
1102
+ static anchorStaleReason(anchor) {
1103
+ const label = `${anchor.kind}${anchor.name === undefined ? '' : `:${anchor.name}`}`;
1104
+ return `${label} 声明为 ${anchor.value},当前不符`;
1105
+ }
1086
1106
  /** {@link verifyAnchors} 的实现体;必须经 `writeScope` 进入。 */
1087
1107
  async verifyAnchorsInner(probes, projectCwd) {
1088
1108
  const tables = [this.requireTable('global')];
@@ -1101,13 +1121,12 @@ export class MemoryEngine extends Service {
1101
1121
  continue; // 未校验:探测不到就不动
1102
1122
  if ((block.stale === true) === !holds)
1103
1123
  continue; // 状态没变
1104
- const label = `${block.anchor.kind}${block.anchor.name === undefined ? '' : `:${block.anchor.name}`}`;
1105
1124
  const updated = holds
1106
1125
  ? { ...block, stale: false, staleReason: undefined }
1107
1126
  : {
1108
1127
  ...block,
1109
1128
  stale: true,
1110
- staleReason: `${label} 声明为 ${block.anchor.value},当前不符`,
1129
+ staleReason: MemoryEngine.anchorStaleReason(block.anchor),
1111
1130
  injected: false,
1112
1131
  injectedAuto: false,
1113
1132
  };
@@ -1452,7 +1471,7 @@ export class MemoryEngine extends Service {
1452
1471
  const status = verdict.quarantined ? 'suggested' : normalized.status;
1453
1472
  // 常驻注入(2026-09-18):显式给值即接管;未审核或已隔离一律不注入(与自动升级同一门槛)
1454
1473
  const injected = status === 'approved' ? patch.injected ?? normalized.injected : false;
1455
- return {
1474
+ const patched = {
1456
1475
  ...block,
1457
1476
  status,
1458
1477
  injected,
@@ -1465,6 +1484,35 @@ export class MemoryEngine extends Service {
1465
1484
  ...(patch.keywords === undefined ? {} : { keywords }),
1466
1485
  updatedAt: Date.now(),
1467
1486
  };
1487
+ // 锚点(0.12.1):省略 → 连 stale 一起保持原值
1488
+ if (patch.anchor === undefined)
1489
+ return patched;
1490
+ // 给 `null` → 清除锚点,并连「因它而标的失效」一起清。别的来源(撤回、被取代)原因文案
1491
+ // 不同,不动——「解除约束」与「撤销作废」是两件事,不能顺手一起抹掉。
1492
+ if (patch.anchor === null) {
1493
+ const fromAnchor = block.anchor !== undefined
1494
+ && block.staleReason === MemoryEngine.anchorStaleReason(block.anchor);
1495
+ return {
1496
+ ...patched,
1497
+ anchor: undefined,
1498
+ ...fromAnchor ? { stale: false, staleReason: undefined } : {},
1499
+ };
1500
+ }
1501
+ // 给值 → 当场校验一次并同步 stale;下面的字段组与 `verifyAnchorsInner` 的失效分支一致
1502
+ const holds = anchorHolds(patch.anchor, this.anchorProbes(projectCwd));
1503
+ if (holds === true)
1504
+ return { ...patched, anchor: patch.anchor, stale: false, staleReason: undefined };
1505
+ if (holds === false) {
1506
+ return {
1507
+ ...patched,
1508
+ anchor: patch.anchor,
1509
+ stale: true,
1510
+ staleReason: MemoryEngine.anchorStaleReason(patch.anchor),
1511
+ injected: false,
1512
+ injectedAuto: false,
1513
+ };
1514
+ }
1515
+ return { ...patched, anchor: patch.anchor }; // 探测不到:不动 stale(未校验 ≠ 失效)
1468
1516
  };
1469
1517
  // 注入位变化单独发一条事件:客户端与面板据此刷新「常驻」列,只发 status 会漏掉它
1470
1518
  const publish = (previous, updated) => {
package/dist/index.js CHANGED
@@ -59,6 +59,16 @@ const RECORD_SCHEMA = {
59
59
  hitCount: { type: 'number' },
60
60
  quarantined: { type: 'boolean' },
61
61
  scope: { type: 'string', description: '来源工作区(④-A):缺省 = 当前会话工作区;`..` / `../..` = 上级工作区。' },
62
+ anchor: {
63
+ type: 'object',
64
+ additionalProperties: false,
65
+ description: '有效性锚点:这个值一变,这条记忆就自动失效。`memory_update` 传新值即改写(当场重新校验),传 `null` 即清除。',
66
+ properties: {
67
+ kind: { type: 'string', required: true, enum: ['env', 'tool-list', 'self-version', 'path-exists'] },
68
+ name: { type: 'string' },
69
+ value: { type: 'string', required: true },
70
+ },
71
+ },
62
72
  },
63
73
  };
64
74
  const HIT_SCHEMA = {
@@ -164,6 +174,7 @@ function recordValue(record) {
164
174
  hitCount: record.hitCount,
165
175
  ...record.quarantined === true ? { quarantined: true } : {},
166
176
  ...record.scope === undefined ? {} : { scope: record.scope },
177
+ ...record.anchor === undefined ? {} : { anchor: record.anchor },
167
178
  };
168
179
  }
169
180
  function hitValue(hit) {
@@ -610,7 +621,7 @@ export async function apply(ctx, config) {
610
621
  }));
611
622
  ctx.tools.register(defineTool({
612
623
  name: 'memory_update',
613
- description: 'Update the content, keywords or persistent-injection switch of one stored memory (e.g. correcting stale facts). The record keeps its review status; content that looks like a credential quarantines it instead. 修改一条记忆的内容、关键词或常驻注入开关(如修正过时信息)。改动后记忆保持原有审核状态;若新内容命中危险规则则转为隔离。',
624
+ description: 'Update the content, keywords, persistent-injection switch or validity anchor of one stored memory (e.g. correcting stale facts, or re-anchoring a memory whose anchor no longer holds). The record keeps its review status; content that looks like a credential quarantines it instead. 修改一条记忆的内容、关键词、常驻注入开关或有效性锚点(如修正过时信息、把失效的锚点改对)。改动后记忆保持原有审核状态;若新内容命中危险规则则转为隔离。',
614
625
  parameters: {
615
626
  id: { type: 'string', required: true, description: 'Exact memory id from memory_list. 记忆 id(来自 memory_list).' },
616
627
  content: { type: 'string', description: 'New content; omit to keep current. 新内容;省略则保留现有内容.' },
@@ -618,6 +629,21 @@ export async function apply(ctx, config) {
618
629
  supersedes: { type: 'string', description: 'Mark the memory with this id as superseded by the current one (the current record is unchanged; only the other gets the void mark). 把该 id 指向的记忆标成「被本条取代」——本条不变,只写对方的作废标记。与 content / retract 互斥。' },
619
630
  retract: { type: 'string', description: 'Retract this memory: give a reason and it is marked void (content kept, no longer injected, still searchable). 撤回本条:给一个原因即标记作废——内容保留、不再进注入,检索仍可检回。与 content / supersedes 互斥。' },
620
631
  injected: { type: 'boolean', description: 'Set or clear the persistent-injection switch; omit to keep the current value. Setting it records a deliberate decision — the memory stops being auto-demoted (repeated hits no longer promote anything). See memory_save for what deserves residency. 设置或清除常驻注入开关;省略则保持原值。给值即视为有意决定——此后不受自动降级影响(反复命中已不再自动开启任何东西)。何时该常驻见 memory_save。' },
632
+ anchor: {
633
+ description: 'Set, replace, or clear the validity anchor; omit to keep the current one. A new value is re-probed immediately: if it holds, a stale mark left by the old anchor is dropped; if it fails, the memory is marked stale and unpinned at once. `null` clears the anchor together with the stale mark that anchor caused. 设置 / 改写 / 清除有效性锚点;省略则保持原值。给值**当场重新探测**——新锚点成立就撤掉旧锚点留下的失效标记,不成立就立刻标失效并撤下常驻;给 `null` 清除锚点,并连同它带来的失效标记一起清。',
634
+ oneOf: [
635
+ {
636
+ type: 'object',
637
+ additionalProperties: false,
638
+ properties: {
639
+ kind: { type: 'string', required: true, enum: ['env', 'tool-list', 'self-version', 'path-exists'], description: 'env = a named environment variable; tool-list = the available tool set; self-version = this plugin version; path-exists = a path that should still exist (`name` = the path, `value` = `present` or `absent`).' },
640
+ name: { type: 'string', description: 'For kind=env: the variable name. For kind=path-exists: the path (relative paths resolve against the session workspace).' },
641
+ value: { type: 'string', required: true, description: 'The value probed at write time.' },
642
+ },
643
+ },
644
+ { type: 'null' },
645
+ ],
646
+ },
621
647
  },
622
648
  output: {
623
649
  schema: RECORD_SCHEMA,
@@ -636,6 +662,10 @@ export async function apply(ctx, config) {
636
662
  ...args.content === undefined ? {} : { content: args.content },
637
663
  ...args.keywords === undefined ? {} : { keywords: args.keywords },
638
664
  ...args.injected === undefined ? {} : { injected: args.injected },
665
+ // 参数已由 tool schema 校验(对象或 null),这里的窄化只为过类型
666
+ ...args.anchor === undefined
667
+ ? {}
668
+ : { anchor: args.anchor },
639
669
  }, cwd).then(recordValue);
640
670
  },
641
671
  presentCall: args => present('Update memory', 'other', args.id),
package/dist/self.js CHANGED
@@ -7,6 +7,8 @@
7
7
  * `prompt_*` 系列,照它读会以为本插件只有记忆工具。
8
8
  * 0.12.0:工具清单 10 → 11(补 `memory_sweep`);删掉「命中累计 2 次自动开启常驻」
9
9
  * (该规则已取消,命中只喂候选提示);注入取舍依据由「最近使用」改为「最近更新」。
10
+ * 0.12.1:锚点那句补上「可以用 memory_update 改写或清除」——此前模型读得到锚点的作用,
11
+ * 却没有任何改它的手段,读起来像一条只能写死一次的东西。
10
12
  */
11
13
  import { createRequire } from 'node:module';
12
14
  export const SELF_VERSION = (() => {
@@ -29,7 +31,7 @@ export const SELF_DESCRIPTION = `[记忆系统自述] 本环境内置跨会话
29
31
  + '③ 得出跨会话仍然有用的结论、路径或坑,而非本次任务的临时细节。'
30
32
  + '**不要记**:临时状态、未经验证的推测、能从代码或文档直接读到的事实(那类记「去哪查」即可)。'
31
33
  + '**怎么写**:一条一件事,并给多角度关键词(同义词、缩写、中英),否则将来检索不到它。'
32
- + '**锚点(可选)**:若这条记忆描述的是「随某个环境值变化的事实」(某工具版本下的行为、某环境变量决定的配置),用 anchor 参数把它绑到那个值上——值变了它会自动失效并标 stale,不会再被当成仍然正确。'
34
+ + '**锚点(可选)**:若这条记忆描述的是「随某个环境值变化的事实」(某工具版本下的行为、某环境变量决定的配置),用 anchor 参数把它绑到那个值上——值变了它会自动失效并标 stale,不会再被当成仍然正确;绑错了用 memory_update 改(当场重新判定,成立即复活),不要了就传 `anchor: null` 清除。当前锚点随记录一起返回。'
33
35
  + '写入即生效(`approved`),人工只在例外时介入;命中密钥/凭据规则的写入会被隔离(不进注入与检索)。'
34
36
  + '**常驻注入**:每轮注入「global + 当前会话工作区」里 approved 且开着常驻开关的记忆单行摘要(形如 `- [memory:id:namespace] 摘要`);全文要靠 memory_search 取。'
35
37
  + '开关只有一条自动规则:**自动升上来的**常驻在 30 天未命中后撤下;命中次数不再自动改变注入状态(它只喂体检报告的候选提示)。**给 memory_save / memory_update 传 `injected` 即显式接管**,此后这条规则也不再适用。'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max-null/dsh-memory",
3
- "version": "0.12.0",
3
+ "version": "0.12.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": {
@@ -67,22 +67,22 @@
67
67
  },
68
68
  "peerDependencies": {
69
69
  "@deepseek-ai/cordis": "^4.0.1",
70
- "@deepseek-ai/dsh-storage": "^0.1.1-rc.1",
71
- "@deepseek-ai/dsh-storage-domain": "^0.1.1-rc.1",
72
- "@deepseek-ai/dsh-storage-json": "^0.1.1-rc.1",
73
- "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.1",
74
- "@deepseek-ai/dsh-tools": "^0.1.1-rc.1",
75
- "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.1"
70
+ "@deepseek-ai/dsh-storage": ">=0.1.1-rc.1 <0.3.0",
71
+ "@deepseek-ai/dsh-storage-domain": ">=0.1.1-rc.1 <0.3.0",
72
+ "@deepseek-ai/dsh-storage-json": ">=0.1.1-rc.1 <0.3.0",
73
+ "@deepseek-ai/dsh-system-prompt": ">=0.1.1-rc.1 <0.3.0",
74
+ "@deepseek-ai/dsh-tools": ">=0.1.1-rc.1 <0.3.0",
75
+ "@deepseek-ai/dsh-typert-protocol": ">=0.1.1-rc.1 <0.3.0"
76
76
  },
77
77
  "devDependencies": {
78
78
  "@deepseek-ai/cordis": "^4.0.1",
79
79
  "@deepseek-ai/cordis-plugin-loader": "^1.0.2",
80
- "@deepseek-ai/dsh-storage": "^0.1.1-rc.1",
81
- "@deepseek-ai/dsh-storage-domain": "^0.1.1-rc.1",
82
- "@deepseek-ai/dsh-storage-json": "^0.1.1-rc.1",
83
- "@deepseek-ai/dsh-system-prompt": "^0.1.1-rc.1",
84
- "@deepseek-ai/dsh-tools": "^0.1.1-rc.1",
85
- "@deepseek-ai/dsh-typert-protocol": "^0.1.1-rc.1",
80
+ "@deepseek-ai/dsh-storage": "0.2.0-rc.1",
81
+ "@deepseek-ai/dsh-storage-domain": "0.2.0-rc.1",
82
+ "@deepseek-ai/dsh-storage-json": "0.2.0-rc.1",
83
+ "@deepseek-ai/dsh-system-prompt": "0.2.0-rc.1",
84
+ "@deepseek-ai/dsh-tools": "0.2.0-rc.1",
85
+ "@deepseek-ai/dsh-typert-protocol": "0.2.0-rc.1",
86
86
  "@types/node": "^26.2.0",
87
87
  "tsx": "^4.0.0",
88
88
  "typescript": "^5.5.0",