@max-null/dsh-memory 0.8.0 → 0.9.1

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
@@ -86,6 +86,8 @@ 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 条常驻因预算未注入:…)`,0.9.1 起列出的是**每条的短摘要**而不是 id——查 id 是什么的那一步最容易省略,省略了就等于没报;**清理干净即自行消失**。面板的注入预览里也能展开看明细。诊断行本身不占预算。
90
+
89
91
  记忆还可声明**有效性锚点**(环境变量 / 工具清单 / 插件版本):所绑的值变化后,这条记忆被标记 `stale` 并撤下常驻,检索结果里也带出失效提示——环境变了,旧结论就不再被当成仍然正确。
90
92
 
91
93
  模型写入不需要人点头,但**人始终能看见并推翻**:面板按「隔离区 / 常驻 / 冷数据」分组,被隔离的记录必须由人放行才会重新进入检索。
package/client.js CHANGED
@@ -196,6 +196,9 @@ var STRINGS = {
196
196
  confirmAll: "\u5168\u90E8\u653E\u884C",
197
197
  injectPreview: "\u6CE8\u5165\u9884\u89C8",
198
198
  contextUsage: "\u4E0A\u4E0B\u6587\u5360\u7528",
199
+ injectionOmitted: "{count} \u6761\u5E38\u9A7B\u8BB0\u5FC6\u672A\u6CE8\u5165\uFF08\u5C55\u5F00\u67E5\u770B\uFF09",
200
+ budgetLabel: "\u9884\u7B97",
201
+ contextUsageDetail: "{self}+{injected} \u5B57\u7B26{budget} \u2248 {tokens} token",
199
202
  keywordsLabel: "\u5173\u952E\u8BCD",
200
203
  suggested: "\u9694\u79BB",
201
204
  approved: "\u5DF2\u751F\u6548",
@@ -249,6 +252,9 @@ var STRINGS = {
249
252
  confirmAll: "Release all",
250
253
  injectPreview: "Injection preview",
251
254
  contextUsage: "Context usage",
255
+ injectionOmitted: "{count} resident memories not injected (expand)",
256
+ budgetLabel: "budget",
257
+ contextUsageDetail: "{self}+{injected} chars{budget} \u2248 {tokens} token",
252
258
  keywordsLabel: "Keywords",
253
259
  suggested: "Quarantined",
254
260
  approved: "Active",
@@ -985,13 +991,23 @@ function MemoryView(props) {
985
991
  const selfChars = preview.self.length;
986
992
  const total = selfChars + preview.chars;
987
993
  const tokens = Math.ceil(total / 2);
988
- const budgetText = preview.budget === null ? "" : ` / \u9884\u7B97 ${preview.budget}`;
989
- const omittedText = preview.omitted > 0 ? `\uFF08\u7701\u7565 ${preview.omitted} \u6761\uFF09` : "";
990
- return `${selfChars}+${preview.chars} \u5B57\u7B26${budgetText}${omittedText} \u2248 ${tokens} token`;
994
+ const budgetText = preview.budget === null ? "" : ` / ${t("budgetLabel")} ${preview.budget}`;
995
+ return t("contextUsageDetail", { self: selfChars, injected: preview.chars, budget: budgetText, tokens });
991
996
  })())
992
997
  ),
993
998
  (0, import_react.createElement)("div", { style: { ...ssid.text, fontSize: 11, whiteSpace: "pre-wrap", wordBreak: "break-all" } }, preview.self),
994
- preview.lines.length === 0 ? (0, import_react.createElement)("div", { style: ssid.muted }, t("empty")) : preview.lines.map((line, index) => (0, import_react.createElement)("div", { key: index, style: { ...ssid.muted, fontSize: 11 } }, line))
999
+ preview.lines.length === 0 ? (0, import_react.createElement)("div", { style: ssid.muted }, t("empty")) : preview.lines.map((line, index) => (0, import_react.createElement)("div", { key: index, style: { ...ssid.muted, fontSize: 11 } }, line)),
1000
+ // 预算诊断(2026-09-18):被预算挡在外面的常驻记忆——展开可见 id 与摘要
1001
+ preview.omittedRecords.length > 0 ? (0, import_react.createElement)(
1002
+ "details",
1003
+ { style: { ...ssid.muted, fontSize: 11 } },
1004
+ (0, import_react.createElement)(
1005
+ "summary",
1006
+ { style: { cursor: "pointer" } },
1007
+ t("injectionOmitted", { count: preview.omitted })
1008
+ ),
1009
+ preview.omittedRecords.map((record, index) => (0, import_react.createElement)("div", { key: index, style: { ...ssid.muted, fontSize: 11, marginTop: 4 } }, record.line))
1010
+ ) : null
995
1011
  ) : null,
996
1012
  // 未选择工作区:工作区视图显示占位(0.3.4 工作区路由语义)
997
1013
  namespace === "workspace" && (props.cwd === void 0 || props.cwd === "") ? (0, import_react.createElement)("div", { style: ssid.empty }, t("noWorkspace")) : listSection
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { MemoryEngine } from "./engine.js";
9
9
  import { MemoryGateway } from "./remote.js";
10
10
  import { mountMemoryApi } from "./routes.js";
11
11
  import { SELF_DESCRIPTION } from "./self.js";
12
- import { DEFAULT_INJECTION_BUDGET, DEFAULT_SUMMARY_CHARS, renderInjection } from "./injection.js";
12
+ import { DEFAULT_INJECTION_BUDGET, DEFAULT_SUMMARY_CHARS, omittedNotice, renderInjection } from "./injection.js";
13
13
  export { MemoryEngine } from "./engine.js";
14
14
  export { MemoryId } from "./engine.js";
15
15
  export { bm25FieldScores, bm25Scores, cosineSimilarity, rrfFuse, tokenize } from "./bm25.js";
@@ -112,11 +112,19 @@ const GUIDANCE = 'Use memory tools for cross-session preferences, habits, and pr
112
112
  // (0.3.4 起工作区记忆就按会话 cwd 路由;provider 是同步回调,只读已
113
113
  // 打开/缓存的表,未打开时本轮工作区部分为空,由 ensureProjectOpen 预热)。
114
114
  // 0.5.2:注入经摘要化 + 预算截断(renderInjection,纯函数)防上下文膨胀。
115
+ // 0.9.0:装不下的条目不再是无声丢弃——末尾追加一行预算诊断(omittedNotice),
116
+ // 报出被挡在外面的 id;它只在有出局时出现,清理干净即消失。
115
117
  function recallText(memory, projectCwd, budget, summaryChars) {
116
118
  const rendered = renderInjection(memory.recallRecords(projectCwd), budget, summaryChars);
117
- if (rendered.lines.length === 0)
118
- return '';
119
- return `Remembered preferences and conventions — apply these:\n${rendered.lines.join('\n')}`;
119
+ const parts = [];
120
+ if (rendered.lines.length > 0) {
121
+ parts.push(`Remembered preferences and conventions — apply these:\n${rendered.lines.join('\n')}`);
122
+ }
123
+ // 全部出局(lines 为空)时诊断照出——那正是最该报的情形
124
+ const notice = omittedNotice(rendered);
125
+ if (notice !== '')
126
+ parts.push(notice);
127
+ return parts.join('\n');
120
128
  }
121
129
  /** 从组装上下文取当前 agent 会话的工作区 cwd(AssembleContext.agent 由 dsh-agent 声明合并提供)。 */
122
130
  function assemblyProjectCwd(assembly) {
@@ -2,13 +2,17 @@
2
2
  * 注入渲染(0.5.2):`memory:recall` 与面板「注入预览」共用的确定性
3
3
  * 渲染——摘要化 + 预算截断。纯函数、无模型调用:注入行 = 每条记忆的
4
4
  * 单行摘要(内容过长取首行截断),超出预算(字符数)时按「最近更新
5
- * 优先」贪心保留,省略计数返回给 UI 提示。
5
+ * 优先」贪心保留;省略明细返回给面板与预算诊断(2026-09-18)。
6
6
  */
7
7
  import type { MemoryRecord } from './engine.ts';
8
8
  /** 摘要截断上限(字符):中英混合场景下 ~40-60 token。 */
9
9
  export declare const DEFAULT_SUMMARY_CHARS = 80;
10
10
  /** 注入预算(字符):0.5.2 起默认开启,防常驻注入随记忆增长膨胀上下文。 */
11
11
  export declare const DEFAULT_INJECTION_BUDGET = 1500;
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;
12
16
  /**
13
17
  * 确定性摘要:取首行,超过 maxChars 截断加省略号。
14
18
  * 空内容/空首行回退为去掉换行后的前 maxChars(保证 any 内容有摘要)。
@@ -23,7 +27,17 @@ export interface InjectionRender {
23
27
  budget: number | null;
24
28
  /** 因预算被省略的候选条数(0 = 全部注入)。 */
25
29
  omitted: number;
26
- /** 实际注入的字符数(摘要 + 标记开销之和)。 */
30
+ /**
31
+ * 被省略条目的明细:`summary` 供预算诊断行报出「是哪条」,`line` 与
32
+ * {@link lines} 同格式、供面板明细直接渲染,`id` 备用。诊断行**不占预算**——
33
+ * 它由 `recallText` 追加在注入行之后,不参与这里的截断。
34
+ */
35
+ omittedRecords: Array<{
36
+ id: string;
37
+ line: string;
38
+ summary: string;
39
+ }>;
40
+ /** 实际注入的字符数(摘要 + 标记开销之和;不含 {@link omittedRecords})。 */
27
41
  chars: number;
28
42
  }
29
43
  /**
@@ -32,3 +46,11 @@ export interface InjectionRender {
32
46
  * 预算为 null 或非正数时全量注入(不过滤)。
33
47
  */
34
48
  export declare function renderInjection(records: readonly MemoryRecord[], budget: number | null, summaryChars: number): InjectionRender;
49
+ /**
50
+ * 预算诊断行(2026-09-18):列出被预算挡在外面的条目,**带短摘要而不是只有 id**——
51
+ * 「主动去查这个 id 是什么」正是最容易省略的那一步,而省略它就等于看不出来。
52
+ * 只报出局者、不报进场者(后者本来就在注入列表里)。**不占注入预算**(调用方把它
53
+ * 追加在注入行之后),清理干净后自行消失(`omitted === 0` 返回空串);超出
54
+ * {@link OMITTED_NOTICE_LIMIT} 只列前 N 个。
55
+ */
56
+ export declare function omittedNotice(rendered: InjectionRender): string;
package/dist/injection.js CHANGED
@@ -2,12 +2,16 @@
2
2
  * 注入渲染(0.5.2):`memory:recall` 与面板「注入预览」共用的确定性
3
3
  * 渲染——摘要化 + 预算截断。纯函数、无模型调用:注入行 = 每条记忆的
4
4
  * 单行摘要(内容过长取首行截断),超出预算(字符数)时按「最近更新
5
- * 优先」贪心保留,省略计数返回给 UI 提示。
5
+ * 优先」贪心保留;省略明细返回给面板与预算诊断(2026-09-18)。
6
6
  */
7
7
  /** 摘要截断上限(字符):中英混合场景下 ~40-60 token。 */
8
8
  export const DEFAULT_SUMMARY_CHARS = 80;
9
9
  /** 注入预算(字符):0.5.2 起默认开启,防常驻注入随记忆增长膨胀上下文。 */
10
10
  export const DEFAULT_INJECTION_BUDGET = 1500;
11
+ /** 诊断行列出的条数上限(2026-09-18):超出只列前 N 个,防这行本身失控。 */
12
+ export const OMITTED_NOTICE_LIMIT = 12;
13
+ /** 诊断行里每条出局者的摘要长度(2026-09-18):够认出是哪条即可。 */
14
+ export const OMITTED_SUMMARY_CHARS = 24;
11
15
  /** 单条注入行的固定标记开销(不含摘要本体)。 */
12
16
  const LINE_OVERHEAD = 27;
13
17
  /**
@@ -36,6 +40,7 @@ export function injectionLine(record, summaryChars) {
36
40
  export function renderInjection(records, budget, summaryChars) {
37
41
  const ordered = [...records].sort((left, right) => (right.lastUsedAt ?? right.updatedAt) - (left.lastUsedAt ?? left.updatedAt));
38
42
  const lines = [];
43
+ const omittedRecords = [];
39
44
  let chars = 0;
40
45
  let omitted = 0;
41
46
  for (const record of ordered) {
@@ -43,10 +48,30 @@ export function renderInjection(records, budget, summaryChars) {
43
48
  const cost = text.length + LINE_OVERHEAD;
44
49
  if (budget !== null && budget > 0 && chars + cost > budget) {
45
50
  omitted++;
51
+ omittedRecords.push({
52
+ id: String(record.id),
53
+ line: text,
54
+ summary: deriveSummary(record.content, OMITTED_SUMMARY_CHARS),
55
+ });
46
56
  continue;
47
57
  }
48
58
  lines.push(text);
49
59
  chars += cost;
50
60
  }
51
- return { lines, budget: budget ?? null, omitted, chars };
61
+ return { lines, budget: budget ?? null, omitted, omittedRecords, chars };
62
+ }
63
+ /**
64
+ * 预算诊断行(2026-09-18):列出被预算挡在外面的条目,**带短摘要而不是只有 id**——
65
+ * 「主动去查这个 id 是什么」正是最容易省略的那一步,而省略它就等于看不出来。
66
+ * 只报出局者、不报进场者(后者本来就在注入列表里)。**不占注入预算**(调用方把它
67
+ * 追加在注入行之后),清理干净后自行消失(`omitted === 0` 返回空串);超出
68
+ * {@link OMITTED_NOTICE_LIMIT} 只列前 N 个。
69
+ */
70
+ export function omittedNotice(rendered) {
71
+ if (rendered.omitted === 0)
72
+ return '';
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})`;
52
77
  }
package/dist/remote.d.ts CHANGED
@@ -31,13 +31,18 @@ export declare class MemoryGateway extends TypertRemoteService {
31
31
  /**
32
32
  * 注入预览(0.3.5 + 0.5.2):开发者查看当前注入到 system prompt 的记忆
33
33
  * 内容——self 自述 + 当前会话工作区(cwd)的 approved+injected 记忆,
34
- * 经摘要化 + 预算截断(与 memory:recall 同源渲染)。
34
+ * 经摘要化 + 预算截断(与 memory:recall 同源渲染)。`omittedRecords`
35
+ * (2026-09-18)供面板展开「哪些常驻没进上下文」。
35
36
  */
36
37
  injectionPreview(cwd?: string, budget?: number | null, summaryChars?: number): Promise<{
37
38
  self: string;
38
39
  lines: string[];
39
40
  budget: number | null;
40
41
  omitted: number;
42
+ omittedRecords: Array<{
43
+ id: string;
44
+ line: string;
45
+ }>;
41
46
  chars: number;
42
47
  }>;
43
48
  }
package/dist/remote.js CHANGED
@@ -112,7 +112,8 @@ let MemoryGateway = (() => {
112
112
  /**
113
113
  * 注入预览(0.3.5 + 0.5.2):开发者查看当前注入到 system prompt 的记忆
114
114
  * 内容——self 自述 + 当前会话工作区(cwd)的 approved+injected 记忆,
115
- * 经摘要化 + 预算截断(与 memory:recall 同源渲染)。
115
+ * 经摘要化 + 预算截断(与 memory:recall 同源渲染)。`omittedRecords`
116
+ * (2026-09-18)供面板展开「哪些常驻没进上下文」。
116
117
  */
117
118
  async injectionPreview(cwd, budget, summaryChars) {
118
119
  if (cwd !== undefined)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@max-null/dsh-memory",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
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": {