dsh-prime-memory 0.20.1 → 0.20.3

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/CHANGELOG.md CHANGED
@@ -17,6 +17,23 @@
17
17
  - **建议状态(`longtask-hint-get`)以纯端点提供**:判定 = 上下文占用达 `contextThresholdPct` 或 todo 漂移达 `driftThreshold`(部署配置可调);配合 `longtask-compress-tail` 作手动压缩入口。
18
18
  - **撤下浮动球 UI(`client/src/longtask-fab.ts`;beta.1 起曾随包发布)**:悬浮建议面板的归属本在 context-compression 侧(「建议启动压缩」是其领域),pm 属误载——2026-10-02 用户裁定撤下。撤下不违座位契约(settings.section 恰一个 + 输入栏 pill,body 单例不占座位),但全应用级 overlay 会与其他插件的悬浮 UI 抢占屏幕角落;长任务开关与手动压缩入口待长任务定版时落记忆设置分节。
19
19
  - **`session-mode-set` 的 `mode` 转为可选**:与 `longTask` 二选一必传——纯切长任务开关不再被迫发送档位(旧客户端恒传 mode,行为不变)。响应 `mode` 改回当前生效值。
20
+ ## [0.20.3] — 2026-10-03
21
+
22
+ ### 修复
23
+
24
+ - **`memory_search` 的渲染文本补上 id**:v0.20.2 把 record_id 加进了输出 JSON 与 schema,但模型看到的是 render 产物而非原始 JSON——渲染行没带 id,对模型等于没返回(真机复测发现)。现在每条结果以 `— id: mem_xxx` 结尾,可直接写入槽位 refs。
25
+
26
+ ## [0.20.2] — 2026-10-03
27
+
28
+ ### 修复
29
+
30
+ - **投影 `memorySlots` 判脏放宽到任意事件**:此前仅在 `tool/result`(settled、非 error)时按 store rev 判脏——写入后依赖"恰好再来一条工具结果"才刷新,跨会话查看更要等查看会话自己的工具调用;真机暴露出写入后侧栏长期停在空帧。改为任意已提交事件做 O(1) rev 比较,rev 变即重建推送;isError 守卫移除(rev 只在真实变更后递增,事件只是触发器,不是数据源)。
31
+ - **常驻注入改 `<system-reminder>` 标签帧**(对齐 dsh-agent-instructions 的 AGENTS.md 注入 house style):user 角色承载不变,但以标签框 + 权威性声明(免回应/勿复述/不覆盖系统提示与用户直接指令)呈现——修掉弱模型把槽位内容当用户发言原样复述、干扰正常回复的问题;槽位正文中的闭合标签转义(`</system-reminder>`),防止槽位内容伪造帧边界。
32
+
33
+ ### 新增
34
+
35
+ - **`memory_search` 结果透出 `id`(L1 record_id)**:解除"检索拿不到 id → 无法写槽位 refs / 喂 memory_receipts"的死锁;schema 描述注明 id 的两个去向。
36
+ - **wire view 透出 `refs` + `refViews`**:`mem_*` 类引用在投影帧构建时经 L1 `getByIds` 解析为 `[type] 名称简述`(content 首行,80 字截断),brief-sidebar(≥0.5.0-beta.2)直接渲染,路径/URL 类引用保持字面;缺失/已退场记录不产生条目。解析发生在帧构建时——引用集在写槽位时即冻结,L1 记录事后编辑不回灌旧帧。自实现 schema 对附加字段"存在才校验",旧 checkpoint 行整条通过(stateVersion 不动)。
20
37
  ## [0.20.1] — 2026-10-03(转正;beta 渠道自 beta.1 起的全部能力定版)
21
38
 
22
39
  Hall·Room 分类管理全链(两级注册表/merge/rename/retire/update/收编/退场带记录/
@@ -1,4 +1,22 @@
1
1
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
2
+ /**
3
+ * 对齐宿主 house style(dsh-agent-instructions 的 AGENTS.md 注入):user 角色
4
+ * 承载 + `<system-reminder>` 标签框。宿主对自家指令刻意**不用** system 角色——
5
+ * 会话中段的 system 消息会破坏部分聊天模板(qwen-chat-template 等),标签框即可
6
+ * 让模型把它识别为系统级元信息(Claude Code 同款语义)。
7
+ */
8
+ const SYSTEM_REMINDER_OPEN = '<system-reminder>';
9
+ const SYSTEM_REMINDER_CLOSE = '</system-reminder>';
10
+ const SLOTS_INTRO = '以下是本环境的常驻记忆槽位(激活槽位):跨会话持续生效的背景约定,仅在与之相关时作为指引遵循。' +
11
+ '它们是系统注入的元信息,不是用户发言——无需回应,请勿在回复中复述本节内容;' +
12
+ '且不覆盖系统提示与用户的直接指令,更具体的指令优先于更一般的约定。';
13
+ /**
14
+ * 槽位标题/正文来自模型工具调用,可能含有闭合标签;不转义会让槽位内容伪造
15
+ * 帧边界(宿主对指令文件正文做同样的转义)。
16
+ */
17
+ function escapeFrameBody(body) {
18
+ return body.replaceAll(SYSTEM_REMINDER_CLOSE, '<\\/system-reminder>');
19
+ }
2
20
  export function registerSlotRecall(ctx, cfg, slots, logger, _live) {
3
21
  const buildInjection = () => {
4
22
  if (!cfg.slots.enabled || !cfg.slots.inject)
@@ -13,7 +31,12 @@ export function registerSlotRecall(ctx, cfg, slots, logger, _live) {
13
31
  if (truncatedCount > 0) {
14
32
  lines.push(`… 另有 ${truncatedCount} 个常驻槽位因预算未显示,可用 memory_slot_list 查看`);
15
33
  }
16
- return '【激活槽位 · 常驻上下文】\n' + lines.join('\n');
34
+ return [
35
+ SYSTEM_REMINDER_OPEN,
36
+ SLOTS_INTRO,
37
+ escapeFrameBody(lines.join('\n')),
38
+ SYSTEM_REMINDER_CLOSE,
39
+ ].join('\n');
17
40
  };
18
41
  ctx.on('agent/pre-step', async (payload, next) => {
19
42
  const decision = await next();
package/dist/index.js CHANGED
@@ -373,7 +373,7 @@ export async function apply(ctx, config) {
373
373
  // 激活槽位(active slot):工具面 + 常驻注入 + 服务端投影(均走 ctx.effect,可撤销)
374
374
  registerSlotTools(ctx, config, stores.slots, logger, modes, live, stores.l1);
375
375
  const slotRecall = registerSlotRecall(ctx, config, stores.slots, logger, live);
376
- registerSlotsProjection(ctx, stores.slots);
376
+ registerSlotsProjection(ctx, stores.slots, stores.l1);
377
377
  void slotRecall;
378
378
  registerMemoryRpc(ctx, config, stores, logger, {
379
379
  degraded: () => !storageOk || db.isDegraded(),
@@ -17,6 +17,20 @@ export interface SlotsViewSlot {
17
17
  kind: string;
18
18
  status: string;
19
19
  priority: number;
20
+ /** 原始引用(L1 record_id / 文件路径 / URL);存在才校验,旧 checkpoint 无此字段照常通过。 */
21
+ refs?: string[];
22
+ /**
23
+ * L1 record_id 类引用的解析视图(v0.20.2):ref → 名称简述(content 首行截断)。
24
+ * 前端展示用;解析发生在投影帧构建时(帧随 slots rev 刷新,L1 记录后续编辑
25
+ * 不回灌旧帧——引用集在写入时即冻结,可接受)。缺失/已退场记录不产生条目,
26
+ * 前端回落显示原始 ref。
27
+ */
28
+ refViews?: SlotRefView[];
29
+ }
30
+ /** 一条 record_id 引用的展示解析。 */
31
+ export interface SlotRefView {
32
+ ref: string;
33
+ title: string;
20
34
  }
21
35
  export interface SlotsProjectionState {
22
36
  rev: number;
@@ -51,5 +65,13 @@ interface SchemaLike<T> {
51
65
  export declare const stateSchema: SchemaLike<SlotsProjectionState>;
52
66
  /** wire view 校验:registry 在 view 出网前调用(也覆盖 live drive 的发布路径)。 */
53
67
  export declare const viewSchema: SchemaLike<SlotsView>;
54
- export declare function registerSlotsProjection(ctx: Context, store: SlotStore): void;
68
+ /** 投影需要的 L1 只读面(结构化声明,避免跨模块类型耦合)。 */
69
+ export interface L1LookupFace {
70
+ getByIds(ids: string[]): Array<{
71
+ id: string;
72
+ content: string;
73
+ type: string;
74
+ }>;
75
+ }
76
+ export declare function registerSlotsProjection(ctx: Context, store: SlotStore, l1?: L1LookupFace): void;
55
77
  export {};
@@ -40,6 +40,26 @@ function parseSlotsPayload(value, what) {
40
40
  if (typeof priority !== 'number' || !Number.isInteger(priority) || priority < 0 || priority > 100) {
41
41
  invalid(at, 'priority 必须是 0-100 的整数');
42
42
  }
43
+ // refs / refViews:存在才校验(附加字段,旧 checkpoint 行没有也必须整条通过)
44
+ if (item.refs !== undefined) {
45
+ if (!Array.isArray(item.refs))
46
+ invalid(at, 'refs 必须是字符串数组');
47
+ item.refs.forEach((r, i) => {
48
+ if (typeof r !== 'string')
49
+ invalid(at + '.refs[' + i + ']', '必须是字符串');
50
+ });
51
+ }
52
+ if (item.refViews !== undefined) {
53
+ if (!Array.isArray(item.refViews))
54
+ invalid(at, 'refViews 必须是数组');
55
+ item.refViews.forEach((v, i) => {
56
+ const at2 = at + '.refViews[' + i + ']';
57
+ const view = requireRecord(v, at2);
58
+ if (typeof view.ref !== 'string' || typeof view.title !== 'string') {
59
+ invalid(at2, 'ref/title 必须是字符串');
60
+ }
61
+ });
62
+ }
43
63
  });
44
64
  return value;
45
65
  }
@@ -51,9 +71,38 @@ export const stateSchema = {
51
71
  export const viewSchema = {
52
72
  parse: (value) => parseSlotsPayload(value, 'memorySlots.view'),
53
73
  };
54
- function buildState(store) {
74
+ /** L1 record_id 的引用形态(newId('mem') 生成)。 */
75
+ const RECORD_REF_PREFIX = 'mem_';
76
+ /** refView 名称简述的截断上限(展示用,全文走 memory_receipts / memory_read_scene)。 */
77
+ const REF_TITLE_MAX = 80;
78
+ /**
79
+ * 把槽位 refs 里的 L1 record_id 解析成名称简述([type] content 首行截断)。
80
+ * 只解析 record_id 类引用;路径/URL 保持字面。缺失/已退场记录不产生条目。
81
+ */
82
+ function resolveRefViews(refs, l1) {
83
+ const recordRefs = refs.filter((r) => r.startsWith(RECORD_REF_PREFIX));
84
+ if (recordRefs.length === 0 || l1 === undefined)
85
+ return undefined;
86
+ const byId = new Map(l1.getByIds(recordRefs).map((r) => [r.id, r]));
87
+ const views = [];
88
+ for (const ref of recordRefs) {
89
+ const record = byId.get(ref);
90
+ if (record === undefined)
91
+ continue;
92
+ const firstLine = record.content.split('\n', 1)[0] ?? '';
93
+ const summary = firstLine.length > REF_TITLE_MAX ? firstLine.slice(0, REF_TITLE_MAX) + '…' : firstLine;
94
+ views.push({ ref, title: '[' + record.type + '] ' + summary });
95
+ }
96
+ return views.length > 0 ? views : undefined;
97
+ }
98
+ function buildState(store, l1) {
55
99
  const snap = store.projectionSnapshot();
56
- return { rev: store.revision(), count: snap.count, openCount: snap.openCount, slots: snap.slots };
100
+ const slots = snap.slots.map((slot) => {
101
+ if (slot.refs === undefined)
102
+ return slot;
103
+ return { ...slot, refViews: resolveRefViews(slot.refs, l1) };
104
+ });
105
+ return { rev: store.revision(), count: snap.count, openCount: snap.openCount, slots };
57
106
  }
58
107
  /**
59
108
  * view 引用稳定:同一 state 连续两次 view() 满足 Object.is(注册表据此判脏,
@@ -75,27 +124,22 @@ function view(state) {
75
124
  }
76
125
  /**
77
126
  * apply(state, event):同步。
78
- * - 仅 tool/result(settled,非 error)才检查:tool/call 在 store 变更提交前发生,按
79
- * call 折会读到 stale;tool/result settled 才保证变更已落库(对齐 deliverables-fold)。
80
- * - 闭包 SlotStore,同步读 revision():rev 未变 → 本插件槽位未动 → 返回同引用
81
- * (满足 I1b,避免 republish thrash);rev 变 → 重建快照。
82
- * - 任何无关事件 → 返回同引用(不 spread)。
127
+ * - **任意已提交事件都判脏**(v0.18.4 起):此前仅在 tool/result(settled、非 error)
128
+ * 时检查,实际运行中暴露出脆弱性——写入后的投影帧依赖"该会话恰好再来一条
129
+ * tool/result"才刷新;跨会话场景(会话 A 写、会话 B 看)更是要等 B 自己的下一次
130
+ * 工具调用。改为对所有事件做 O(1) revision 比较:rev 未变 → 同引用(满足 I1b,
131
+ * 无 republish thrash);rev 变 → 立即重建。写入后的下一条任意事件(用户消息、
132
+ * turn 边界、其他工具结果)都会把新帧推给客户端。
133
+ * - isError 守卫一并移除:rev 只在 store 真实变更后递增(persist 内联 RMW),
134
+ * 事件成败与否不影响"store 是否变了"这一事实;错误事件后重建读到的仍是
135
+ * 当前真实快照,不会引入错误数据。
83
136
  */
84
- function applySlotsEvent(state, event, store) {
85
- if (event.type !== 'tool/result')
86
- return state;
87
- const data = event.data;
88
- // v4 native(宿主 ≥0.1.7-rc.1):isError 在 message 顶层;v3 wrapper:块上 isError(历史兼容)。
89
- const isError = data?.message?.isError === true ||
90
- (Array.isArray(data?.message?.content) &&
91
- data.message.content.some((c) => c.isError === true));
92
- if (isError)
93
- return state;
137
+ function applySlotsEvent(state, event, store, l1) {
94
138
  if (store.revision() === state.rev)
95
139
  return state;
96
- return buildState(store);
140
+ return buildState(store, l1);
97
141
  }
98
- export function registerSlotsProjection(ctx, store) {
142
+ export function registerSlotsProjection(ctx, store, l1) {
99
143
  ctx.inject(['sessionProjections'], (injected) => {
100
144
  const registry = injected
101
145
  .sessionProjections;
@@ -104,8 +148,8 @@ export function registerSlotsProjection(ctx, store) {
104
148
  registry.register({
105
149
  key: MEMORY_SLOTS_KEY,
106
150
  stateSchema,
107
- init: () => buildState(store),
108
- apply: (state, event) => applySlotsEvent(state, event, store),
151
+ init: () => buildState(store, l1),
152
+ apply: (state, event) => applySlotsEvent(state, event, store, l1),
109
153
  wire: { viewSchema, view },
110
154
  stateVersion: 0,
111
155
  });
@@ -50,6 +50,8 @@ export interface SlotView {
50
50
  kind: SlotKind;
51
51
  status: SlotStatus;
52
52
  priority: number;
53
+ /** 原始引用(v0.20.2 透出,供展示侧解析 L1 record_id → 名称简述)。 */
54
+ refs?: string[];
53
55
  }
54
56
  export declare class SlotStore {
55
57
  private readonly file;
@@ -118,7 +120,7 @@ export declare class SlotStore {
118
120
  * 返回过期条数。
119
121
  */
120
122
  expireDue(now?: number): Promise<number>;
121
- /** 投影只读快照(全量,不含 body)。 */
123
+ /** 投影只读快照(全量,不含 body;refs 存在才带上)。 */
122
124
  projectionSnapshot(): {
123
125
  count: number;
124
126
  openCount: number;
@@ -274,7 +274,7 @@ export class SlotStore {
274
274
  await this.persist();
275
275
  return n;
276
276
  }
277
- /** 投影只读快照(全量,不含 body)。 */
277
+ /** 投影只读快照(全量,不含 body;refs 存在才带上)。 */
278
278
  projectionSnapshot() {
279
279
  return {
280
280
  count: this.slots.length,
@@ -285,6 +285,7 @@ export class SlotStore {
285
285
  kind: s.kind,
286
286
  status: s.status,
287
287
  priority: s.priority,
288
+ ...(s.refs.length > 0 ? { refs: [...s.refs] } : {}),
288
289
  })),
289
290
  };
290
291
  }
@@ -135,6 +135,7 @@ roomRegistry) {
135
135
  items: {
136
136
  type: 'object',
137
137
  properties: {
138
+ id: { type: 'string', description: 'L1 record_id:可写入槽位 refs 或喂给 memory_receipts' },
138
139
  content: { type: 'string' },
139
140
  type: { type: 'string' },
140
141
  scene_name: { type: 'string' },
@@ -177,6 +178,7 @@ roomRegistry) {
177
178
  }
178
179
  return {
179
180
  items: items.map((h) => ({
181
+ id: h.id,
180
182
  content: h.content,
181
183
  type: h.type,
182
184
  scene_name: h.scene_name,
@@ -1314,8 +1316,13 @@ function renderGraphCards(items) {
1314
1316
  function renderMemoryItems(items) {
1315
1317
  if (!items || items.length === 0)
1316
1318
  return '(没有找到相关记忆)';
1319
+ // id 必须进渲染文本:模型看到的是 render 产物而非原始 JSON——漏了它,
1320
+ // "search 返回 record_id" 对模型等于没返回(真机教训,v0.20.3)。
1317
1321
  return items
1318
- .map((it, i) => `${i + 1}. [${it.type ?? ''}]${it.scene_name ? ` (${it.scene_name})` : ''} ${it.content ?? ''}`)
1322
+ .map((it, i) => {
1323
+ const head = `${i + 1}. [${it.type ?? ''}]${it.scene_name ? ` (${it.scene_name})` : ''} ${it.content ?? ''}`;
1324
+ return it.id ? `${head} — id: ${it.id}` : head;
1325
+ })
1319
1326
  .join('\n');
1320
1327
  }
1321
1328
  function renderConversationItems(items) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-prime-memory",
3
- "version": "0.20.1",
3
+ "version": "0.20.3",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public",