@max-null/dsh-memory 0.2.1 → 0.3.7

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.js CHANGED
@@ -18,7 +18,8 @@ export function MemoryId(id) {
18
18
  }
19
19
  const blockSchema = z.object({
20
20
  namespace: z.enum(['global', 'project']),
21
- status: z.enum(['suggested', 'auto', 'suggest']),
21
+ status: z.enum(['suggested', 'approved', 'auto', 'suggest']),
22
+ injected: z.boolean().optional(),
22
23
  content: z.string(),
23
24
  keywords: z.array(z.string()),
24
25
  createdAt: z.number(),
@@ -32,8 +33,30 @@ function memorySpec(name) {
32
33
  tables: { blocks: domainTable(blockSchema) },
33
34
  });
34
35
  }
36
+ /**
37
+ * 旧 schema 迁移(0.3.0,设计文档「迁移规则」):
38
+ * - 旧 `auto` → approved + injected:true(行为不变:仍常驻注入)
39
+ * - 旧 `suggest` → suggested + injected:false
40
+ * - 缺 injected 的 approved → injected:false(新写路径兜底)
41
+ * - 其余 → suggested + injected:false
42
+ */
43
+ function normalizeBlock(block) {
44
+ if (block.status === 'auto')
45
+ return { status: 'approved', injected: true };
46
+ if (block.status === 'suggest')
47
+ return { status: 'suggested', injected: false };
48
+ return { status: block.status, injected: block.injected ?? false };
49
+ }
35
50
  function toRecord(id, block) {
36
- return { id: MemoryId(id), ...block };
51
+ return {
52
+ id: MemoryId(id),
53
+ namespace: block.namespace,
54
+ ...normalizeBlock(block),
55
+ content: block.content,
56
+ keywords: block.keywords,
57
+ createdAt: block.createdAt,
58
+ updatedAt: block.updatedAt,
59
+ };
37
60
  }
38
61
  /** Harness-home root for `global` memories. */
39
62
  function globalRoot() {
@@ -45,56 +68,107 @@ function projectRoot() {
45
68
  }
46
69
  /**
47
70
  * Cross-session plaintext memory over the storage hub, split by namespace:
48
- * `global` lives in the harness home, `project` in the project folder.
71
+ * `global` lives in the harness home, `project` in the session's workspace
72
+ * folder (`<workspace>/.dsh/storages`, follows the repository) — 0.3.4:
73
+ * project memory routes by the CALLER's workspace cwd (工具/面板按当前会话
74
+ * 工作区路由), not the process launch dir.
49
75
  */
50
76
  export class MemoryEngine extends Service {
51
77
  config;
52
78
  static inject = ['storage'];
53
79
  globalTable;
54
- projectTable;
80
+ /** project 域按工作区 cwd 懒打开 + 缓存(多会话并发各工作区独立)。 */
81
+ projectTables = new Map();
82
+ projectFacilities = new Map();
83
+ /** backend 只注册一次(registry 重名抛 duplicate;reload 清缓存后不得重复注册)。 */
84
+ registeredProjectBackends = new Set();
55
85
  facility;
56
86
  constructor(ctx, config = {}) {
57
87
  super(ctx, 'memory');
58
88
  this.config = config;
59
89
  }
60
90
  async [Service.init]() {
61
- await this.openDomains();
62
- this.ctx.effect(() => () => { void this.facility?.closeAll(); }, 'memory.domainsClose');
63
- }
64
- /** 打开两个存储域(init 与 reload 共用)。 */
65
- async openDomains() {
91
+ // backend 只注册一次:registry 对重名注册抛 duplicate-backend
92
+ // (storage/tests/registry.spec 实测),reload 不得重复注册。
66
93
  const globalBackend = new JsonStorageBackend(this.config.globalRoot ?? globalRoot());
67
- const projectBackend = new JsonStorageBackend(this.config.projectRoot ?? projectRoot());
68
94
  this.ctx.storage.backend.register('memory-global', globalBackend);
69
- this.ctx.storage.backend.register('memory-project', projectBackend);
70
- const facility = new DomainFacility(this.ctx, {
71
- backend: 'memory-global',
72
- routes: { memory_project: 'memory-project' },
73
- });
95
+ await this.openGlobalFacility();
96
+ this.ctx.effect(() => () => {
97
+ void this.facility?.closeAll();
98
+ for (const facility of this.projectFacilities.values())
99
+ void facility.closeAll();
100
+ }, 'memory.domainsClose');
101
+ }
102
+ /** 打开全局存储域(init 与 reload 共用;backend 复用已注册实例)。 */
103
+ async openGlobalFacility() {
104
+ const facility = new DomainFacility(this.ctx, { backend: 'memory-global', routes: {} });
74
105
  const globalDomain = await facility.open(memorySpec('memory'));
75
- const projectDomain = await facility.open(memorySpec('memory_project'));
76
106
  this.facility = facility;
77
107
  this.globalTable = globalDomain.table('blocks');
78
- this.projectTable = projectDomain.table('blocks');
108
+ }
109
+ /** 工作区 project 存储根(<workspace>/.dsh/storages,随 git 分享)。 */
110
+ projectRootFor(cwd) {
111
+ return join(cwd, '.dsh', 'storages');
112
+ }
113
+ /** 稳定 backend 名(registry 重名抛错,按 cwd hash 唯一化;只允许 [a-z0-9_],domain 名校验)。 */
114
+ projectBackendName(cwd) {
115
+ let h = 5381;
116
+ const text = cwd.toLowerCase(); // Windows 路径大小写不敏感
117
+ for (let i = 0; i < text.length; i++)
118
+ h = ((h << 5) + h + text.charCodeAt(i)) | 0;
119
+ return `memory_project_${Math.abs(h).toString(36)}`;
79
120
  }
80
121
  /**
81
- * 强制重载存储:关闭两域后重开,放弃内存缓存重读文件。
82
- * 供外部编辑记忆文件后刷新(JsonStorageBackend 打开时加载一次,
83
- * 无 watch——2026-08-19 用户实测面板不感知外部编辑)。
122
+ * 按工作区 cwd 取 project 表(懒打开 + 缓存)。无 cwd(未选工作区)
123
+ * 返回 undefined——调用方(工具/面板)按此跳过 project 部分。
124
+ */
125
+ async projectTableFor(projectCwd) {
126
+ if (projectCwd === undefined || projectCwd === '')
127
+ return undefined;
128
+ const key = join(projectCwd); // 规范化(Windows 大小写/尾斜杠)
129
+ const cached = this.projectTables.get(key);
130
+ if (cached !== undefined)
131
+ return cached;
132
+ const backendName = this.projectBackendName(key);
133
+ if (!this.registeredProjectBackends.has(backendName)) {
134
+ const backend = new JsonStorageBackend(this.projectRootFor(key));
135
+ this.ctx.storage.backend.register(backendName, backend);
136
+ this.registeredProjectBackends.add(backendName);
137
+ }
138
+ const facility = new DomainFacility(this.ctx, { backend: backendName, routes: {} });
139
+ const domain = await facility.open(memorySpec(`memory_project_${backendName}`));
140
+ const table = domain.table('blocks');
141
+ this.projectFacilities.set(key, facility);
142
+ this.projectTables.set(key, table);
143
+ return table;
144
+ }
145
+ /**
146
+ * 强制重载存储:关闭全局域与全部已开工作区域后重开(unit 释放后
147
+ * backend 重读文件),放弃内存缓存。供外部编辑记忆文件后刷新
148
+ * (JsonStorageBackend 打开时加载一次,无 watch——2026-08-19 实测)。
84
149
  */
85
150
  async reload() {
86
151
  await this.facility?.closeAll();
87
- await this.openDomains();
152
+ for (const facility of this.projectFacilities.values())
153
+ await facility.closeAll();
154
+ this.projectTables.clear();
155
+ this.projectFacilities.clear();
156
+ await this.openGlobalFacility();
88
157
  }
89
158
  /** Create one record in `suggested` status — never self-promoting. */
90
- async remember(input) {
159
+ async remember(input, projectCwd) {
91
160
  const namespace = input.namespace ?? 'global';
92
- const table = this.tableFor(namespace);
161
+ const table = namespace === 'project'
162
+ ? await this.projectTableFor(projectCwd)
163
+ : this.tableFor('global');
164
+ if (table === undefined)
165
+ throw new Error('cannot write project memory without a workspace cwd');
93
166
  const id = randomUUID();
94
167
  const now = Date.now();
95
168
  const block = {
96
169
  namespace,
97
170
  status: 'suggested',
171
+ injected: false,
98
172
  content: input.content,
99
173
  keywords: (input.keywords ?? []).map(keyword => keyword.toLowerCase()),
100
174
  createdAt: now,
@@ -105,12 +179,13 @@ export class MemoryEngine extends Service {
105
179
  this.ctx.emit('memory/changed', { operation: 'remembered', record });
106
180
  return record;
107
181
  }
108
- list(filter) {
109
- const records = this.allRecords(filter?.namespace);
110
- return records.filter(record => (filter?.status === undefined || record.status === filter.status));
182
+ async list(filter, projectCwd) {
183
+ const records = await this.allRecords(filter?.namespace, projectCwd);
184
+ return records.filter(record => (filter?.status === undefined || record.status === filter.status)
185
+ && (filter?.injected === undefined || record.injected === filter.injected));
111
186
  }
112
- search(query, filter) {
113
- const records = this.list(filter);
187
+ async search(query, filter, projectCwd) {
188
+ const records = await this.list(filter, projectCwd);
114
189
  const docs = records.map(record => `${record.content} ${record.keywords.join(' ')}`);
115
190
  const scores = bm25Scores(query, docs);
116
191
  return records
@@ -118,44 +193,146 @@ export class MemoryEngine extends Service {
118
193
  .filter(hit => hit.score > 0)
119
194
  .sort((left, right) => right.score - left.score);
120
195
  }
121
- async forget(id) {
196
+ async forget(id, projectCwd) {
122
197
  if (await this.requireTable('global').delete(id)) {
123
198
  this.ctx.emit('memory/changed', { operation: 'forgotten', id });
124
199
  return true;
125
200
  }
126
- if (await this.requireTable('project').delete(id)) {
201
+ const project = await this.projectTableFor(projectCwd);
202
+ if (project !== undefined && await project.delete(id)) {
127
203
  this.ctx.emit('memory/changed', { operation: 'forgotten', id });
128
204
  return true;
129
205
  }
130
206
  return false;
131
207
  }
132
- async setStatus(id, status) {
208
+ async setStatus(id, status, projectCwd) {
133
209
  const global = this.requireTable('global').get(id);
134
210
  if (global !== undefined) {
135
- const updated = { ...global, status, updatedAt: Date.now() };
211
+ const updated = {
212
+ ...global,
213
+ status,
214
+ // 旧数据可能缺 injected,写回时补全(读时迁移值)
215
+ injected: normalizeBlock(global).injected,
216
+ updatedAt: Date.now(),
217
+ };
136
218
  await this.requireTable('global').put(id, updated);
137
219
  const record = toRecord(id, updated);
138
220
  this.ctx.emit('memory/changed', { operation: 'status', id, status });
139
221
  return record;
140
222
  }
141
- const project = this.requireTable('project').get(id);
223
+ const project = await this.projectTableFor(projectCwd);
142
224
  if (project !== undefined) {
143
- const updated = { ...project, status, updatedAt: Date.now() };
144
- await this.requireTable('project').put(id, updated);
225
+ const block = project.get(id);
226
+ if (block !== undefined) {
227
+ const updated = {
228
+ ...block,
229
+ status,
230
+ injected: normalizeBlock(block).injected,
231
+ updatedAt: Date.now(),
232
+ };
233
+ await project.put(id, updated);
234
+ const record = toRecord(id, updated);
235
+ this.ctx.emit('memory/changed', { operation: 'status', id, status });
236
+ return record;
237
+ }
238
+ }
239
+ throw new Error(`cannot set status of unknown memory '${id}'`);
240
+ }
241
+ /**
242
+ * 注入维度开关(0.3.0):只改 injected,不动审核状态。供 UI 面板
243
+ * 「常驻注入」开关调用(remote.setInjected)。
244
+ */
245
+ async setInjected(id, injected, projectCwd) {
246
+ const global = this.requireTable('global').get(id);
247
+ if (global !== undefined) {
248
+ const updated = {
249
+ ...global,
250
+ status: normalizeBlock(global).status,
251
+ injected,
252
+ updatedAt: Date.now(),
253
+ };
254
+ await this.requireTable('global').put(id, updated);
145
255
  const record = toRecord(id, updated);
146
- this.ctx.emit('memory/changed', { operation: 'status', id, status });
256
+ this.ctx.emit('memory/changed', { operation: 'injected', id, injected });
147
257
  return record;
148
258
  }
149
- throw new Error(`cannot set status of unknown memory '${id}'`);
259
+ const project = await this.projectTableFor(projectCwd);
260
+ if (project !== undefined) {
261
+ const block = project.get(id);
262
+ if (block !== undefined) {
263
+ const updated = {
264
+ ...block,
265
+ status: normalizeBlock(block).status,
266
+ injected,
267
+ updatedAt: Date.now(),
268
+ };
269
+ await project.put(id, updated);
270
+ const record = toRecord(id, updated);
271
+ this.ctx.emit('memory/changed', { operation: 'injected', id, injected });
272
+ return record;
273
+ }
274
+ }
275
+ throw new Error(`cannot set injected of unknown memory '${id}'`);
276
+ }
277
+ /**
278
+ * 修改一条记忆的内容/关键词(0.3.1 整理记忆用)。内容被模型改动后
279
+ * 必须重新人工审核:status 重置为 suggested(自然停止注入——注入仅对
280
+ * approved 生效);injected 保留原值(审核通过后注入开关原样恢复)。
281
+ */
282
+ async update(id, patch, projectCwd) {
283
+ const applyPatch = (block) => {
284
+ const normalized = normalizeBlock(block);
285
+ return {
286
+ ...block,
287
+ status: 'suggested',
288
+ injected: normalized.injected,
289
+ ...(patch.content === undefined ? {} : { content: patch.content }),
290
+ ...(patch.keywords === undefined ? {} : { keywords: patch.keywords.map(keyword => keyword.toLowerCase()) }),
291
+ updatedAt: Date.now(),
292
+ };
293
+ };
294
+ const global = this.requireTable('global').get(id);
295
+ if (global !== undefined) {
296
+ const updated = applyPatch(global);
297
+ await this.requireTable('global').put(id, updated);
298
+ const record = toRecord(id, updated);
299
+ this.ctx.emit('memory/changed', { operation: 'status', id, status: 'suggested' });
300
+ return record;
301
+ }
302
+ const project = await this.projectTableFor(projectCwd);
303
+ if (project !== undefined) {
304
+ const block = project.get(id);
305
+ if (block !== undefined) {
306
+ const updated = applyPatch(block);
307
+ await project.put(id, updated);
308
+ const record = toRecord(id, updated);
309
+ this.ctx.emit('memory/changed', { operation: 'status', id, status: 'suggested' });
310
+ return record;
311
+ }
312
+ }
313
+ throw new Error(`cannot update unknown memory '${id}'`);
150
314
  }
151
- allRecords(namespace) {
152
- if (namespace === 'project')
153
- return this.recordsOf(this.requireTable('project'));
315
+ /**
316
+ * 注入专用(0.3.4):systemPrompt context 是同步回调、无会话 cwd 可及,
317
+ * 只返回 global 的 approved+injected(工作区记忆走检索/面板)。
318
+ */
319
+ recallRecords() {
320
+ return this.recordsOf(this.requireTable('global'))
321
+ .filter(record => record.status === 'approved' && record.injected);
322
+ }
323
+ async allRecords(namespace, projectCwd) {
324
+ if (namespace === 'project') {
325
+ const project = await this.projectTableFor(projectCwd);
326
+ if (project === undefined)
327
+ return [];
328
+ return this.recordsOf(project);
329
+ }
154
330
  if (namespace === 'global')
155
331
  return this.recordsOf(this.requireTable('global'));
332
+ const project = await this.projectTableFor(projectCwd);
156
333
  return [
157
334
  ...this.recordsOf(this.requireTable('global')),
158
- ...this.recordsOf(this.requireTable('project')),
335
+ ...(project === undefined ? [] : this.recordsOf(project)),
159
336
  ];
160
337
  }
161
338
  recordsOf(table) {
@@ -165,7 +342,7 @@ export class MemoryEngine extends Service {
165
342
  return this.requireTable(namespace);
166
343
  }
167
344
  requireTable(namespace) {
168
- const table = namespace === 'global' ? this.globalTable : this.projectTable;
345
+ const table = namespace === 'global' ? this.globalTable : undefined;
169
346
  if (table === undefined)
170
347
  throw new Error('memory engine is not started yet');
171
348
  return table;
package/dist/index.js CHANGED
@@ -7,18 +7,21 @@
7
7
  import { defineTool } from '@deepseek-ai/dsh-tools';
8
8
  import { MemoryEngine } from "./engine.js";
9
9
  import { MemoryGateway } from "./remote.js";
10
+ import { mountMemoryApi } from "./routes.js";
11
+ import { SELF_DESCRIPTION } from "./self.js";
10
12
  export { MemoryEngine } from "./engine.js";
11
13
  export { MemoryId } from "./engine.js";
12
14
  export { bm25Scores, tokenize } from "./bm25.js";
13
15
  export const name = 'dsh-memory';
14
- export const inject = ['storage', 'systemPrompt', 'tools'];
16
+ export const inject = ['storage', 'systemPrompt', 'tools', 'webServer', 'webRuntime'];
15
17
  const RECORD_SCHEMA = {
16
18
  type: 'object',
17
19
  additionalProperties: false,
18
20
  properties: {
19
21
  id: { type: 'string', required: true },
20
22
  namespace: { type: 'string', required: true, enum: ['global', 'project'] },
21
- status: { type: 'string', required: true, enum: ['suggested', 'auto', 'suggest'] },
23
+ status: { type: 'string', required: true, enum: ['suggested', 'approved'] },
24
+ injected: { type: 'boolean', required: true },
22
25
  content: { type: 'string', required: true },
23
26
  keywords: { type: 'array', required: true, items: { type: 'string' } },
24
27
  createdAt: { type: 'number', required: true },
@@ -38,6 +41,7 @@ function recordValue(record) {
38
41
  id: String(record.id),
39
42
  namespace: record.namespace,
40
43
  status: record.status,
44
+ injected: record.injected,
41
45
  content: record.content,
42
46
  keywords: record.keywords,
43
47
  createdAt: record.createdAt,
@@ -47,17 +51,34 @@ function recordValue(record) {
47
51
  function hitValue(hit) {
48
52
  return { record: recordValue(hit.record), score: hit.score };
49
53
  }
54
+ // 0.3.0:审核语义双语(host 侧 GUIDANCE 无法跟随 DSH locale 动态切换,
55
+ // 采用中英双语都写、模型自取——设计文档「风险与注意」)。
50
56
  const GUIDANCE = 'Use memory tools for cross-session preferences, habits, and project conventions. '
57
+ + '记忆工具用于跨会话的偏好、习惯与项目约定。'
51
58
  + 'memory_save always records a suggestion (`suggested`) and never makes it effective itself — '
52
- + 'a human confirms it. When earlier context may be relevant, call memory_search to recall it. '
53
- + 'Every memory is plaintext and inspectable with memory_list; memory_forget removes one.';
59
+ + 'a human approves it. memory_save 永远只写入建议(`suggested`),不会自行生效——需人工审核通过。'
60
+ + 'Approval only marks the content reviewed; whether it is injected every turn is a separate '
61
+ + 'human-controlled switch (`injected`). 审核通过只代表内容被认可;是否每轮常驻注入由独立的'
62
+ + '人工开关(`injected`)控制。'
63
+ + 'When earlier context may be relevant, call memory_search to recall it — reviewable memories '
64
+ + '(`suggested`) are searchable too. 相关历史上下文可用 memory_search 检索——待审核的记忆也可检索。'
65
+ + 'Every memory is plaintext and inspectable with memory_list; memory_forget removes one. '
66
+ + '所有记忆均为明文,可用 memory_list 查看;memory_forget 删除一条。';
67
+ // 0.3.4:注入只注入 global 的 approved+injected(systemPrompt context 是
68
+ // 同步回调、无会话 cwd 可及);工作区(project)记忆按会话 cwd 路由,靠
69
+ // memory_search / 面板访问。
54
70
  function recallText(memory) {
55
- const auto = memory.list({ status: 'auto' });
71
+ const auto = memory.recallRecords();
56
72
  if (auto.length === 0)
57
73
  return '';
58
74
  const lines = auto.map(record => `- [memory:${String(record.id)}] ${record.content}`);
59
75
  return `Remembered preferences and conventions — apply these:\n${lines.join('\n')}`;
60
76
  }
77
+ /** 从工具执行上下文取调用会话的工作区 cwd(0.3.4 工作区路由)。 */
78
+ function execProjectCwd(exec) {
79
+ return exec
80
+ ?.agent?.session?.header?.cwd;
81
+ }
61
82
  function renderJson(value) {
62
83
  return [{ type: 'text', text: JSON.stringify(value) }];
63
84
  }
@@ -67,6 +88,8 @@ function present(title, kind, rawInput) {
67
88
  export async function apply(ctx, config) {
68
89
  await ctx.plugin(MemoryEngine, config);
69
90
  await ctx.plugin(MemoryGateway);
91
+ // 0.3.6:自带面板数据通道(HTTP,独立包无需 Typert 构建产物)
92
+ mountMemoryApi(ctx);
70
93
  const memory = ctx.get('memory');
71
94
  if (memory === undefined)
72
95
  throw new Error('memory engine failed to register');
@@ -75,6 +98,13 @@ export async function apply(ctx, config) {
75
98
  order: 115,
76
99
  text: GUIDANCE,
77
100
  });
101
+ // 0.3.2:记忆机制自述——常驻注入(不落用户存储),随发版更新。
102
+ // order 49 < recall(50):机制说明在具体记忆内容之前。
103
+ ctx.systemPrompt.context({
104
+ name: 'memory:self',
105
+ order: 49,
106
+ text: SELF_DESCRIPTION,
107
+ });
78
108
  ctx.systemPrompt.context({
79
109
  name: 'memory:recall',
80
110
  order: 50,
@@ -92,51 +122,53 @@ export async function apply(ctx, config) {
92
122
  schema: RECORD_SCHEMA,
93
123
  render: (_args, value) => renderJson(value),
94
124
  },
95
- execute(args, _exec) {
125
+ execute(args, exec) {
96
126
  return memory.remember({
97
127
  content: args.content,
98
128
  ...args.namespace === undefined ? {} : { namespace: args.namespace },
99
129
  ...args.keywords === undefined ? {} : { keywords: args.keywords },
100
- }).then(recordValue);
130
+ }, execProjectCwd(exec)).then(recordValue);
101
131
  },
102
132
  presentCall: args => present('Save memory', 'other', args.content),
103
133
  }));
104
134
  ctx.tools.register(defineTool({
105
135
  name: 'memory_list',
106
- description: 'List every stored memory, optionally filtered by namespace or status. Every memory is plaintext and inspectable.',
136
+ description: 'List every stored memory, optionally filtered by namespace, status, or injected switch. Every memory is plaintext and inspectable. 列出全部记忆,可按 namespace/status/injected 过滤;均为明文可查。',
107
137
  parameters: {
108
- namespace: { type: 'string', enum: ['global', 'project'], description: 'Restrict to one namespace.' },
109
- status: { type: 'string', enum: ['suggested', 'auto', 'suggest'], description: 'Restrict to one status.' },
138
+ namespace: { type: 'string', enum: ['global', 'project'], description: 'Restrict to one namespace. 限定单个命名空间.' },
139
+ status: { type: 'string', enum: ['suggested', 'approved'], description: 'Restrict to one review status. 限定审核状态(suggested=待审核 / approved=已审核).' },
140
+ injected: { type: 'boolean', description: 'Restrict by the persistent-injection switch. 按常驻注入开关过滤.' },
110
141
  },
111
142
  output: {
112
143
  schema: { type: 'array', items: RECORD_SCHEMA },
113
144
  render: (_args, value) => renderJson(value),
114
145
  },
115
- execute(args, _exec) {
116
- return Promise.resolve(memory.list({
146
+ execute(args, exec) {
147
+ return memory.list({
117
148
  ...args.namespace === undefined ? {} : { namespace: args.namespace },
118
149
  ...args.status === undefined ? {} : { status: args.status },
119
- }).map(recordValue));
150
+ ...args.injected === undefined ? {} : { injected: args.injected },
151
+ }, execProjectCwd(exec)).then(records => records.map(recordValue));
120
152
  },
121
153
  presentCall: () => present('List memories', 'read'),
122
154
  }));
123
155
  ctx.tools.register(defineTool({
124
156
  name: 'memory_search',
125
- description: 'Recall stored memories by keyword. Deterministic literal matching — a miss means no stored term matched the query.',
157
+ description: 'Recall stored memories by keyword. Deterministic literal matching — a miss means no stored term matched the query. 按关键词检索记忆(含待审核条目)。',
126
158
  parameters: {
127
- query: { type: 'string', required: true, description: 'Keyword query.' },
128
- namespace: { type: 'string', enum: ['global', 'project'], description: 'Restrict to one namespace.' },
129
- status: { type: 'string', enum: ['suggested', 'auto', 'suggest'], description: 'Restrict to one status.' },
159
+ query: { type: 'string', required: true, description: 'Keyword query. 关键词查询.' },
160
+ namespace: { type: 'string', enum: ['global', 'project'], description: 'Restrict to one namespace. 限定单个命名空间.' },
161
+ status: { type: 'string', enum: ['suggested', 'approved'], description: 'Restrict to one review status. 限定审核状态.' },
130
162
  },
131
163
  output: {
132
164
  schema: { type: 'array', items: HIT_SCHEMA },
133
165
  render: (_args, value) => renderJson(value),
134
166
  },
135
- execute(args, _exec) {
136
- return Promise.resolve(memory.search(args.query, {
167
+ execute(args, exec) {
168
+ return memory.search(args.query, {
137
169
  ...args.namespace === undefined ? {} : { namespace: args.namespace },
138
170
  ...args.status === undefined ? {} : { status: args.status },
139
- }).map(hitValue));
171
+ }, execProjectCwd(exec)).then(hits => hits.map(hitValue));
140
172
  },
141
173
  presentCall: args => present('Search memory', 'read', args.query),
142
174
  }));
@@ -150,23 +182,43 @@ export async function apply(ctx, config) {
150
182
  schema: { type: 'object', additionalProperties: false, properties: { deleted: { type: 'boolean', required: true } } },
151
183
  render: (_args, value) => renderJson(value),
152
184
  },
153
- execute(args, _exec) {
154
- return memory.forget(args.id).then(deleted => ({ deleted }));
185
+ execute(args, exec) {
186
+ return memory.forget(args.id, execProjectCwd(exec)).then(deleted => ({ deleted }));
155
187
  },
156
188
  presentCall: args => present('Forget memory', 'other', args.id),
157
189
  }));
190
+ ctx.tools.register(defineTool({
191
+ name: 'memory_update',
192
+ description: 'Update the content or keywords of one stored memory (e.g. correcting stale facts). The record is re-marked `suggested` — the human must review it again before it is approved/injected again. 修改一条记忆的内容或关键词(如修正过时信息)。改动后该记忆重置为待审核(suggested),需人工再次审核;常驻注入开关保留原值(审核通过后恢复)。',
193
+ parameters: {
194
+ id: { type: 'string', required: true, description: 'Exact memory id from memory_list. 记忆 id(来自 memory_list).' },
195
+ content: { type: 'string', description: 'New content; omit to keep current. 新内容;省略则保留现有内容.' },
196
+ keywords: { type: 'array', items: { type: 'string' }, description: 'New keywords; omit to keep current. 新关键词;省略则保留现有.' },
197
+ },
198
+ output: {
199
+ schema: RECORD_SCHEMA,
200
+ render: (_args, value) => renderJson(value),
201
+ },
202
+ execute(args, exec) {
203
+ return memory.update(args.id, {
204
+ ...args.content === undefined ? {} : { content: args.content },
205
+ ...args.keywords === undefined ? {} : { keywords: args.keywords },
206
+ }, execProjectCwd(exec)).then(recordValue);
207
+ },
208
+ presentCall: args => present('Update memory', 'other', args.id),
209
+ }));
158
210
  ctx.tools.register(defineTool({
159
211
  name: 'memory_confirm',
160
- description: 'Confirm a suggested memory so it becomes effective (`auto`). Only call this when the human explicitly asks to confirm a memory; never self-promote a suggestion.',
212
+ description: 'Approve a suggested memory so it is marked human-reviewed (`approved`). Approval does NOT enable persistent injection — whether a memory is injected every turn is a separate human-controlled switch (`injected`). Only call this when the human explicitly asks to approve a memory; never self-promote a suggestion. 将待审核记忆标记为已审核(approved)。审核通过不改变注入状态——是否每轮常驻注入由独立的人工开关(injected)控制。仅在用户明确要求审核某条记忆时调用;模型不得自我提升。',
161
213
  parameters: {
162
- id: { type: 'string', required: true, description: 'Exact memory id from memory_list.' },
214
+ id: { type: 'string', required: true, description: 'Exact memory id from memory_list. 记忆 id(来自 memory_list).' },
163
215
  },
164
216
  output: {
165
217
  schema: RECORD_SCHEMA,
166
218
  render: (_args, value) => renderJson(value),
167
219
  },
168
- execute(args, _exec) {
169
- return memory.setStatus(args.id, 'auto').then(recordValue);
220
+ execute(args, exec) {
221
+ return memory.setStatus(args.id, 'approved', execProjectCwd(exec)).then(recordValue);
170
222
  },
171
223
  presentCall: args => present('Confirm memory', 'other', args.id),
172
224
  }));
package/dist/remote.d.ts CHANGED
@@ -13,13 +13,28 @@ import type { MemoryFilter, MemoryHit, MemoryRecord } from './engine.ts';
13
13
  export declare class MemoryGateway extends TypertRemoteService {
14
14
  static inject: string[];
15
15
  constructor(ctx: Context);
16
- /** List every stored memory, optionally filtered by namespace or status. */
17
- list(filter?: MemoryFilter): MemoryRecord[];
16
+ /** List every stored memory, optionally filtered by namespace/status/injected and workspace cwd. */
17
+ list(filter?: MemoryFilter, cwd?: string): Promise<MemoryRecord[]>;
18
18
  /** Recall memories by keyword; deterministic literal matching. */
19
- search(query: string, filter?: MemoryFilter): MemoryHit[];
20
- /** Confirm a suggested memory so it becomes effective (`auto`). */
21
- confirm(id: string): Promise<MemoryRecord>;
19
+ search(query: string, filter?: MemoryFilter, cwd?: string): Promise<MemoryHit[]>;
20
+ /** Approve a suggested memory (review status `suggested` → `approved`); does not change injection. */
21
+ confirm(id: string, cwd?: string): Promise<MemoryRecord>;
22
+ /**
23
+ * Toggle the persistent-injection switch (`injected`) — the UI panel's
24
+ * 「常驻注入」 switch; does not change the review status.
25
+ */
26
+ setInjected(id: string, injected: boolean, cwd?: string): Promise<MemoryRecord>;
22
27
  /** Delete one stored memory by id. */
23
- forget(id: string): Promise<boolean>;
28
+ forget(id: string, cwd?: string): Promise<boolean>;
29
+ /** Force-reload storage files (external edits; JsonStorageBackend has no watch). */
30
+ reload(cwd?: string): Promise<MemoryRecord[]>;
31
+ /**
32
+ * 注入预览(0.3.5):开发者查看当前注入到 system prompt 的记忆内容
33
+ * ——self 自述 + 实际注入的 global approved+injected 记忆。
34
+ */
35
+ injectionPreview(): {
36
+ self: string;
37
+ injected: MemoryRecord[];
38
+ };
24
39
  }
25
40
  export default MemoryGateway;