dsh-prime-memory 0.18.1 → 0.18.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/CHANGELOG.en.md CHANGED
@@ -22,6 +22,14 @@ This file covers the **0.12.0** release notes and the current **unreleased** cha
22
22
  - **Silent first-write loss regression**: lock files are created before the write; when the target's parent directory did not exist yet, `open('wx')` failed with ENOENT and stores swallowed it as a warn. `rmwJson` now `ensureDir`s before acquiring the lock (included from the main line).
23
23
  - Also includes everything from the main line: file-layer hardening (atomic writes / read-side classification / version fail-closed / file locking / path safety — see [Unreleased] entries).
24
24
 
25
+ ## [0.18.2] — 2026-09-27
26
+
27
+ ### Fixed
28
+
29
+ - **Session format v4 signature adaptation (host >= 0.1.7-rc.1)**: the two memory-recall injection sites (cross-session recall in `hooks/recall.ts`, slot persistent injection in `hooks/slot-recall.ts`) no longer use the legacy signature `source: { kind: 'plugin', plugin: 'memory', form: 'recall' }` that the v4 host rejects (`SessionFormatError`, whole round fails — the source of "error on memory recall"). They now use the producer-owned `{ kind: 'plugin:memory', form: 'recall' }`. `form`, content and timing unchanged. Evidence: `source()` in `@deepseek-ai/dsh-session-format-v3-to-v4@0.1.7-rc.2` only checks that kind is non-empty and not `'plugin'`; companion fields are not validated.
30
+ - **Dual-shape read side**: the recall-share signature predicate `isOwnRecallSource` accepts both the new signature and legacy v3 rows — fixing only the write side would silently zero the "memory recall share" in the occupancy panel. `MessageSourceMap` is augmented via module merging to register `plugin:memory` (dsh-llm 0.1.7's map is `user|model|tool|'system-prompt'` with no plugin catch-all; merge-extensible per producer).
31
+ - **tool-result v4 native adaptation (N1)**: evidence text reading in `store/evidence-source.ts` gains a native branch (v4 first-class `role:'tool'` message: content is text/reasoning blocks directly, `isError` at message top level; the host's `assertBlock` hard-rejects legacy `tool-result` wrapper blocks). The legacy block-descend path is kept for historical logs — otherwise evidence text would be silently empty on v4. The isError guard in `projection/slots.ts` is natively adapted as well.
32
+
25
33
  ## [Unreleased]
26
34
 
27
35
  ### Added
package/CHANGELOG.ja.md CHANGED
@@ -9,6 +9,14 @@
9
9
 
10
10
  本ファイルは **0.12.0** リリースノートと現在の**未リリース**変更の日本語版です。全履歴は [CHANGELOG.md](./CHANGELOG.md)(中文)を参照してください。
11
11
 
12
+ ## [0.18.2] — 2026-09-27
13
+
14
+ ### 修正
15
+
16
+ - **セッション形式 v4 署名対応(ホスト >= 0.1.7-rc.1)**:記憶 recall 注入 2 箇所(セッション横断 recall `hooks/recall.ts`、スロット常駐注入 `hooks/slot-recall.ts`)で、v4 ホストが拒否する旧署名 `source: { kind: 'plugin', plugin: 'memory', form: 'recall' }`(`SessionFormatError` でラウンド全体が失敗——「記憶呼び出し時のエラー」の原因)を廃止し、producer-owned 署名 `{ kind: 'plugin:memory', form: 'recall' }` に変更。`form`・内容・タイミングは不変。根拠:`@deepseek-ai/dsh-session-format-v3-to-v4@0.1.7-rc.2` の `source()` は kind が非空かつ `'plugin'` でないことのみ検証し、随伴フィールドは検証しない。
17
+ - **読み取り側の双形状互換**:recall 份额推定の署名判定 `isOwnRecallSource` は新署名と v3 旧行の両方を受け付ける——書き側だけ直すと占有パネルの「記憶 recall 份额」が静かに 0 になる。`MessageSourceMap` はモジュール拡張で `plugin:memory` を登録(dsh-llm 0.1.7 の map は `user|model|tool|'system-prompt'` で plugin catch-all なし、producer ごとの merge-extensible)。
18
+ - **tool-result v4 native 対応(N1)**:`store/evidence-source.ts` の取证テキスト読み取りに native 分岐を追加(v4 の一等 `role:'tool'` メッセージ:content は text/reasoning ブロック直接、`isError` は message 最上位。ホストの `assertBlock` は旧 `tool-result` wrapper ブロックを硬拒否)。旧ブロック経路は歴史互換として残存——未対応だと v4 で取证テキストが静かに空になる。`projection/slots.ts` の isError ガードも native 化。
19
+
12
20
  ## [Unreleased]
13
21
 
14
22
  ### 追加
package/CHANGELOG.ko.md CHANGED
@@ -9,6 +9,14 @@
9
9
 
10
10
  이 파일은 **0.12.0** 릴리스 노트와 현재 **미출시** 변경 사항의 한국어판입니다. 전체 이력은 [CHANGELOG.md](./CHANGELOG.md)(中文)를 참조하세요.
11
11
 
12
+ ## [0.18.2] — 2026-09-27
13
+
14
+ ### 수정
15
+
16
+ - **세션 포맷 v4 서명 대응(호스트 >= 0.1.7-rc.1)**: 기억 recall 주입 2곳(세션 간 recall `hooks/recall.ts`, 슬롯 상시 주입 `hooks/slot-recall.ts`)에서 v4 호스트가 거부하는 구형 서명 `source: { kind: 'plugin', plugin: 'memory', form: 'recall' }` 사용을 중단하고(`SessionFormatError`로 라운드 전체 실패——"기억 호출 시 오류"의 원인) producer-owned 서명 `{ kind: 'plugin:memory', form: 'recall' }`로 변경했습니다. `form`·내용·시점은 불변. 근거: `@deepseek-ai/dsh-session-format-v3-to-v4@0.1.7-rc.2`의 `source()`는 kind가 비어 있지 않고 `'plugin'`이 아닌지만 검증하며 수반 필드는 검증하지 않습니다.
17
+ - **읽기 측 이중 형상 호환**: recall 점유율 추정 서명 술어 `isOwnRecallSource`는 신규 서명과 v3 구형 행을 모두 받아들입니다——쓰기 측만 고치면 점유 패널의 "기억 recall 점유율"이 조용히 0이 됩니다. `MessageSourceMap`은 모듈 병합으로 `plugin:memory`를 등록(dsh-llm 0.1.7의 map은 `user|model|tool|'system-prompt'`으로 plugin catch-all 없음, producer별 merge-extensible).
18
+ - **tool-result v4 native 대응(N1)**: `store/evidence-source.ts`의取证 텍스트 읽기에 native 분기 추가(v4 일급 `role:'tool'` 메시지: content는 text/reasoning 블록 직접, `isError`는 message 최상위. 호스트 `assertBlock`은 구형 `tool-result` wrapper 블록을 강제 거부). 구형 블록 경로는 역사 호환으로 유지——미대응 시 v4에서取证 텍스트가 조용히 빈 값이 됩니다. `projection/slots.ts`의 isError 가드도 native화.
19
+
12
20
  ## [Unreleased]
13
21
 
14
22
  ### 추가
package/CHANGELOG.md CHANGED
@@ -6,6 +6,14 @@
6
6
  > **UI 截图约定**:带界面变化的条目在 `assets/changelog/<版本号>/<两位编号>-<简述>.png`
7
7
  > 存真机截图,并在条目内以相对路径引用,读者可在更新日志里直接看到新版本 UI 的样子。
8
8
 
9
+ ## [0.18.2] — 2026-09-27
10
+
11
+ ### 修复
12
+
13
+ - **会话格式 v4 署名适配(宿主 ≥0.1.7-rc.1)**:两处记忆召回注入(跨会话召回 `hooks/recall.ts`、槽位常驻注入 `hooks/slot-recall.ts`)不再使用 v4 宿主拒绝的旧署名 `source: { kind: 'plugin', plugin: 'memory', form: 'recall' }`(触发 `SessionFormatError` 整轮失败——即"调用记忆时报错"的来源),改为 producer-owned 署名 `{ kind: 'plugin:memory', form: 'recall' }`。`form` 与注入内容、时机均不变。取证依据:`@deepseek-ai/dsh-session-format-v3-to-v4@0.1.7-rc.2` 的 `source()` 只查 kind 非空且 ≠ `'plugin'`,不校验伴随字段。
14
+ - **读侧双形状兼容**:召回份额估算的署名判据 `isOwnRecallSource` 同时接受新署名与 v3 旧行——只改写侧会让占用面板"记忆召回份额"静默归零(无异常无报错)。`MessageSourceMap` 经模块增强登记 `plugin:memory`(dsh-llm 0.1.7 的 map 为 `user|model|tool|'system-prompt'`,无 plugin catch-all,merge-extensible per-producer)。
15
+ - **tool-result v4 native 适配(N1)**:`store/evidence-source.ts` 的取证文本读取新增 native 分支(v4 一等 `role:'tool'` 消息:content 直接为 text/reasoning 块、`isError` 在 message 顶层;宿主 `assertBlock` 硬拒旧 `tool-result` wrapper 块),旧块下钻路径保留作历史兼容——否则 v4 下取证文本静默恒空。`projection/slots.ts` 的 isError 守卫同步 native 化。
16
+
9
17
  ## [Unreleased]
10
18
 
11
19
  ### 新增
@@ -28,6 +28,16 @@ import { type OccupancyLedger } from '../util/context-occupancy.js';
28
28
  * 从会话消息构建召回查询(纯函数):末尾 N 条 + 总长截断,空输入返回空串。
29
29
  * 全史拼接会让 MATCH 表达式随会话长度线性膨胀(整会话累计二次方成本)。
30
30
  */
31
+ /**
32
+ * 本插件 recall 注入的署名判据(v4 `plugin:memory` 与 v3 旧行双形状;宿主迁移负责
33
+ * 升格旧行)。导出供双形状单测钉住——估算函数(estimateRecallFromStorage /
34
+ * estimateRecallTokens)对旧形状漏判会让"记忆召回份额"静默归零。
35
+ */
36
+ export declare function isOwnRecallSource(src: {
37
+ kind?: unknown;
38
+ plugin?: unknown;
39
+ form?: unknown;
40
+ } | undefined | null): boolean;
31
41
  export declare function buildRecallQuery(messages: Array<{
32
42
  content: unknown;
33
43
  }>, tailMessages?: number, maxChars?: number): string;
@@ -27,6 +27,14 @@ const RECALL_QUERY_MAX_CHARS = 2_000;
27
27
  * 从会话消息构建召回查询(纯函数):末尾 N 条 + 总长截断,空输入返回空串。
28
28
  * 全史拼接会让 MATCH 表达式随会话长度线性膨胀(整会话累计二次方成本)。
29
29
  */
30
+ /**
31
+ * 本插件 recall 注入的署名判据(v4 `plugin:memory` 与 v3 旧行双形状;宿主迁移负责
32
+ * 升格旧行)。导出供双形状单测钉住——估算函数(estimateRecallFromStorage /
33
+ * estimateRecallTokens)对旧形状漏判会让"记忆召回份额"静默归零。
34
+ */
35
+ export function isOwnRecallSource(src) {
36
+ return !!src && src.form === 'recall' && (src.kind === 'plugin:memory' || (src.kind === 'plugin' && src.plugin === 'memory'));
37
+ }
30
38
  export function buildRecallQuery(messages, tailMessages = RECALL_QUERY_TAIL_MESSAGES, maxChars = RECALL_QUERY_MAX_CHARS) {
31
39
  const tail = messages.slice(-tailMessages);
32
40
  let text = tail.map((m) => blocksToText(m.content)).join(' ').trim();
@@ -298,10 +306,11 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
298
306
  logger.info(`[memory] 召回注入 ${lines.length} 条 L1(mode=${mode},query="${query.slice(0, 30).replace(/\n/g, ' ')}…",agent=${payload.agent.id},消息侧)`);
299
307
  const injection = createUserMessage({
300
308
  content: [{ type: 'text', text }],
301
- // plugin 字段是宿主 UI 的署名后缀("上下文注入 · memory")——用展示友好的
302
- // 子系统名,不用 cordis id(dsh-memory);kind:'plugin' 的标题恒为通用
303
- // "上下文注入"(专用"跨会话召回"标题仅留给 session-reference 来源)
304
- source: { kind: 'plugin', plugin: 'memory', form: 'recall' },
309
+ // v4(宿主 ≥0.1.7-rc.1)署名为 producer-owned kind:plugin:<子系统名>。
310
+ // 用展示友好的子系统名 memory,不用 cordis id(dsh-memory);宿主 UI 标题
311
+ // 规则随之变化(不再有 catch-all 'plugin' 的通用"上下文注入"标题)。
312
+ // v3 旧行(kind:'plugin'+plugin 字段)由宿主 v3→v4 迁移升格,读侧双形状兼容。
313
+ source: { kind: 'plugin:memory', form: 'recall' },
305
314
  });
306
315
  // 入账在成功构造注入消息之后、返回 enter 之前——任何前置抛错路径账目零扰动
307
316
  const led = ledgerFor(payload.agent.id);
@@ -342,6 +351,8 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
342
351
  return '';
343
352
  return body ? `${body}\n\n${MEMORY_TOOLS_GUIDE}` : MEMORY_TOOLS_GUIDE;
344
353
  };
354
+ /** 本插件 recall 注入的署名判据(v4 `plugin:memory` 与 v3 旧行双形状;宿主迁移负责升格旧行)。 */
355
+ const isOwnRecall = isOwnRecallSource;
345
356
  async function estimateRecallFromStorage(sessionId) {
346
357
  if (storedEstimateCache.has(sessionId))
347
358
  return storedEstimateCache.get(sessionId) ?? null;
@@ -358,7 +369,7 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
358
369
  if (ev.type !== 'user/message')
359
370
  continue;
360
371
  const src = ev.data?.source;
361
- if (!src || src.kind !== 'plugin' || src.plugin !== 'memory' || src.form !== 'recall')
372
+ if (!isOwnRecall(src))
362
373
  continue;
363
374
  let chars = 0;
364
375
  for (const b of ev.data?.content ?? []) {
@@ -395,7 +406,7 @@ export function registerRecall(ctx, cfg, stores, logger, live, modes, dataDir) {
395
406
  continue;
396
407
  const msg = ev.data;
397
408
  const src = msg?.source;
398
- if (!src || src.kind !== 'plugin' || src.plugin !== 'memory' || src.form !== 'recall')
409
+ if (!isOwnRecall(src) || !msg)
399
410
  continue;
400
411
  let chars = 0;
401
412
  for (const b of msg.content ?? []) {
@@ -30,8 +30,9 @@ export function registerSlotRecall(ctx, cfg, slots, logger, _live) {
30
30
  return decision;
31
31
  const injection = createUserMessage({
32
32
  content: [{ type: 'text', text }],
33
- // form 受宿主类型约束(仅 recall/snapshot/notice/...),槽位常驻注入沿用 'recall' 分组
34
- source: { kind: 'plugin', plugin: 'memory', form: 'recall' },
33
+ // v4 producer-owned kind(宿主 ≥0.1.7-rc.1);form 沿用 'recall' 分组
34
+ // (宿主 ContextFormed 的 recall 变体无伴随字段)
35
+ source: { kind: 'plugin:memory', form: 'recall' },
35
36
  });
36
37
  return { kind: 'enter', messages: [injection, ...decision.messages] };
37
38
  }
@@ -114,7 +114,7 @@ export async function relabelPass(deps, overrides = {}, opts = {}) {
114
114
  for (let i = 0; i < batch.length; i += LLM_CHUNK)
115
115
  chunks.push(batch.slice(i, i + LLM_CHUNK));
116
116
  let doneCount = 0;
117
- let taggedPool = [];
117
+ const taggedPool = [];
118
118
  for (let i = 0; i < chunks.length; i++) {
119
119
  if (overBudget()) {
120
120
  stats.deferred += batch.length - doneCount;
@@ -85,8 +85,10 @@ function applySlotsEvent(state, event, store) {
85
85
  if (event.type !== 'tool/result')
86
86
  return state;
87
87
  const data = event.data;
88
- const isError = Array.isArray(data?.message?.content) &&
89
- data.message.content.some((c) => c.isError === true);
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));
90
92
  if (isError)
91
93
  return state;
92
94
  if (store.revision() === state.rev)
@@ -120,7 +120,21 @@ export function projectEventText(event) {
120
120
  * 也只会得到空串。必须下钻一层到 `block.content`。
121
121
  */
122
122
  function toolResultText(data) {
123
- const content = data?.message?.content;
123
+ const message = data?.message;
124
+ if (message === undefined || typeof message !== 'object')
125
+ return '';
126
+ // v4 native(宿主 ≥0.1.7-rc.1):一等 role:'tool' 消息,content 直接是
127
+ // text/reasoning 块、isError 提升到 message 顶层——旧块下钻路径对 native 形状
128
+ // 全部 continue,取证文本会静默恒空(本函数头注自证"取证不行")。native
129
+ // 分支是强制新增,不是可选兼容。
130
+ if (message.role === 'tool') {
131
+ const text = blocksToText(message['content']);
132
+ if (text.trim() === '')
133
+ return '';
134
+ return message.isError === true ? `[工具报错] ${text}` : text;
135
+ }
136
+ // v3 wrapper(未迁移日志的历史兼容):下钻 block.content。
137
+ const content = message['content'];
124
138
  if (!Array.isArray(content))
125
139
  return '';
126
140
  const parts = [];
@@ -24,7 +24,7 @@ if (typeof data.dbPath !== 'string' || data.dbPath === '') {
24
24
  port.close();
25
25
  }
26
26
  else {
27
- let db = null;
27
+ let db;
28
28
  try {
29
29
  db = new MemoryDb(data.dbPath, data.dimensions ?? 0);
30
30
  db.init();
package/dist/types.d.ts CHANGED
@@ -235,3 +235,20 @@ export interface L1Hit {
235
235
  priority?: number;
236
236
  family?: MemoryFamily;
237
237
  }
238
+ /**
239
+ * dsh-llm ≥0.1.7-rc.1(会话格式 v4)的 producer-owned source kind 登记。
240
+ *
241
+ * v4 移除了 `MessageSourceMap` 的 catch-all `plugin` kind(message.d.ts 注释明言
242
+ * "each producer declares its own `kind` in its own module"),各生产者须在自己的
243
+ * 模块里以 declaration merging 登记自有 kind;未登记则 `createUserMessage({ source })`
244
+ * 在类型层报 TS2345(封闭联合,无 index signature)。本仓两处写侧(recall 注入 /
245
+ * 槽位常驻注入)使用 `plugin:memory` + `form:'recall'`(ContextFormed 的 recall
246
+ * 变体无伴随字段,0.1.7-rc.2 message.d.ts:75-93 实证)。
247
+ */
248
+ declare module '@deepseek-ai/dsh-llm' {
249
+ interface MessageSourceMap {
250
+ 'plugin:memory': {
251
+ kind: 'plugin:memory';
252
+ } & import('@deepseek-ai/dsh-llm').ContextFormed;
253
+ }
254
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-prime-memory",
3
- "version": "0.18.1",
3
+ "version": "0.18.2",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public",