@jacklika/dsh-memory-curator 0.1.0-alpha.10 → 0.1.0-alpha.12
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 +2 -2
- package/README.zh.md +2 -2
- package/lib/index.js +44 -14
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -4,8 +4,8 @@ Curator tools for the DSH memory vault. This package is a member of the `@jackli
|
|
|
4
4
|
|
|
5
5
|
## Tools
|
|
6
6
|
|
|
7
|
-
- `memory_recall(query)` — Search the vault for prior notes before starting a task. Returns `{ query, hits, total }`, where `total` is the vault-wide match count passed through from `wiki_search` and can exceed `hits.length` when
|
|
8
|
-
- `memory_capture(title, summary, ...)` — Persist durable knowledge after significant work, with conflict detection and optional user approval. Returns `{ written, id, mode, conflict, version, created?, updated?, committed? }`: `version` is the SHA-1 of the persisted note text, `created` is present on first capture, `updated` is present on appends, and `committed` is present when `memory-git` completed the commit.
|
|
7
|
+
- `memory_recall(query)` — Search the vault for prior notes before starting a task. Returns `{ query, hits, total, truncated }`, where `total` is the vault-wide match count passed through from `wiki_search` and can exceed `hits.length` when either layer caps its results, and `truncated` is `true` whenever it did — recall reports its own 20-hit presentation cap, not only the search layer's — so a caller can tell "these are all the matches" from "there are more". Per-turn auto-capture notes from `memory-anchor` are hidden from the hit list and subtracted from `total`, so bookkeeping never crowds out curated knowledge; the match anchors on the notes namespace, so a curated note whose name merely shares the prefix is still returned.
|
|
8
|
+
- `memory_capture(title, summary, ...)` — Persist durable knowledge after significant work, with conflict detection and optional user approval. Set `requireExisting: true` for an update-only capture: it updates a note that must already exist, appends when `mode` is `append` (the default), and fails instead of creating a missing note. Because changing that note is the declared intent, no conflict approval is requested. Returns `{ written, id, mode, conflict, version, created?, updated?, committed? }`: `version` is the SHA-1 of the persisted note text, `created` is present on first capture, `updated` is present on appends, and `committed` is present when `memory-git` completed the commit. `conflict` reports a detected match that needed a policy decision, so a successful update-only capture reports `false`.
|
|
9
9
|
|
|
10
10
|
## Note ids
|
|
11
11
|
|
package/README.zh.md
CHANGED
|
@@ -4,8 +4,8 @@ DSH 记忆库的策展工具。本包是 `@jacklika/dsh-memory` Bundle 的成员
|
|
|
4
4
|
|
|
5
5
|
## 工具
|
|
6
6
|
|
|
7
|
-
- `memory_recall(query)` — 在任务开始前搜索知识库中的相关笔记。返回 `{ query, hits, total }`,其中 `total` 是从 `wiki_search`
|
|
8
|
-
- `memory_capture(title, summary, ...)` —
|
|
7
|
+
- `memory_recall(query)` — 在任务开始前搜索知识库中的相关笔记。返回 `{ query, hits, total, truncated }`,其中 `total` 是从 `wiki_search` 透传的全库命中数;任一层截断(搜索层上限或 recall 自身的 20 条展示上限)都会使 `total` 大于 `hits.length` 且 `truncated` 为 `true`——调用方因此能区分“这就是全部命中”与“还有更多”。`memory-anchor` 写入的逐回合自动入库笔记会从命中列表中隐藏,并从 `total` 中扣除,记账内容不会挤占策展知识;匹配锚定在策展区命名空间上,仅名字前缀相同的策展笔记仍会返回。
|
|
8
|
+
- `memory_capture(title, summary, ...)` — 在有意义的任务结束后持久化知识,支持冲突检测与可选的用户审批。设置 `requireExisting: true` 表示只更新、不新建:目标笔记必须已存在,`mode` 为默认的 `append` 时追加写入,笔记缺失时直接失败而不会创建。由于“更新这篇笔记”本身就是声明的意图,此类调用不会请求冲突审批。返回 `{ written, id, mode, conflict, version, created?, updated?, committed? }`:`version` 是持久化笔记文本的 SHA-1 指纹;首次入库返回 `created`;追加写入返回 `updated`;挂载 `memory-git` 完成提交时还会返回 `committed`。`conflict` 表示检测到的命中需要策略裁决,因此成功执行的只更新捕获会返回 `false`。
|
|
9
9
|
|
|
10
10
|
## 笔记 id
|
|
11
11
|
|
package/lib/index.js
CHANGED
|
@@ -123,17 +123,24 @@ function dedupeHits(hits) {
|
|
|
123
123
|
* @param now - Beijing-time timestamp used for `created`/`updated`.
|
|
124
124
|
* @returns the merged frontmatter to serialize.
|
|
125
125
|
*/
|
|
126
|
-
async function
|
|
127
|
-
let existing;
|
|
126
|
+
async function readFrontmatter(ctx, exec, id) {
|
|
128
127
|
try {
|
|
129
128
|
const note = await callTool(ctx, exec, 'wiki_read', { id });
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
129
|
+
return note !== null && typeof note === 'object' && note.frontmatter !== null && typeof note.frontmatter === 'object'
|
|
130
|
+
? note.frontmatter
|
|
131
|
+
: {};
|
|
133
132
|
}
|
|
134
|
-
catch {
|
|
135
|
-
//
|
|
133
|
+
catch (error) {
|
|
134
|
+
// Only a missing note means a first capture: `wiki_read` reports it as
|
|
135
|
+
// "no such note". Any other failure (permission denied, a directory in the
|
|
136
|
+
// way) must surface instead of silently turning an update into a replace.
|
|
137
|
+
if (error.message.includes('no such note'))
|
|
138
|
+
return undefined;
|
|
139
|
+
throw error;
|
|
136
140
|
}
|
|
141
|
+
}
|
|
142
|
+
async function overwriteFrontmatter(ctx, exec, id, title, tags, now) {
|
|
143
|
+
const existing = await readFrontmatter(ctx, exec, id);
|
|
137
144
|
const frontmatter = { ...existing, title, tags };
|
|
138
145
|
const created = existing?.created;
|
|
139
146
|
frontmatter.created = typeof created === 'string' && created !== '' ? created : now;
|
|
@@ -156,7 +163,7 @@ export function apply(ctx, config) {
|
|
|
156
163
|
const approval = ctx.get('approval');
|
|
157
164
|
ctx.effect(() => ctx.tools.register(defineTool({
|
|
158
165
|
name: 'memory_recall',
|
|
159
|
-
description: 'Search the memory vault for prior notes related to the current task or topic. Call this at the start of a development task to avoid asking the user to repeat context that is already captured. Query terms are OR-matched and ranked by field-weighted relevance, so partial matches still return. Returns the matching note ids, titles, scores, and backlinks, plus total — how many notes matched before the result cap,
|
|
166
|
+
description: 'Search the memory vault for prior notes related to the current task or topic. Call this at the start of a development task to avoid asking the user to repeat context that is already captured. Query terms are OR-matched and ranked by field-weighted relevance, so partial matches still return. Returns the matching note ids, titles, scores, and backlinks, plus total — how many notes matched before the result cap — and truncated, which is true when hits omits matches. A total above the returned hit count also means the list was truncated.',
|
|
160
167
|
parameters: {
|
|
161
168
|
query: {
|
|
162
169
|
type: 'string',
|
|
@@ -197,16 +204,20 @@ export function apply(ctx, config) {
|
|
|
197
204
|
// from the total so curated numbers stay truthful.
|
|
198
205
|
const total = search.total - (search.hits.length - curated.length);
|
|
199
206
|
ctx.logger?.debug(`memory-curator: recall query=${args.query} hits=${curated.length} total=${total}`);
|
|
207
|
+
const hits = curated.slice(0, 20);
|
|
200
208
|
return {
|
|
201
209
|
query: args.query,
|
|
202
|
-
hits
|
|
210
|
+
hits,
|
|
203
211
|
total,
|
|
212
|
+
// `wiki_search` reports its own cap; recall caps again at 20, so the
|
|
213
|
+
// page is incomplete whenever either cap bit.
|
|
214
|
+
truncated: search.truncated || curated.length > hits.length,
|
|
204
215
|
};
|
|
205
216
|
},
|
|
206
217
|
})));
|
|
207
218
|
ctx.effect(() => ctx.tools.register(defineTool({
|
|
208
219
|
name: 'memory_capture',
|
|
209
|
-
description: 'Persist durable knowledge to the memory vault. Call this at the end of a significant task to capture decisions, discovered APIs, project conventions, fixes, or user preferences. It checks for conflicting notes, asks for approval when a conflict is found, and writes the captured note to the shared curated zone by default.',
|
|
220
|
+
description: 'Persist durable knowledge to the memory vault. Call this at the end of a significant task to capture decisions, discovered APIs, project conventions, fixes, or user preferences. It checks for conflicting notes, asks for approval when a conflict is found, and writes the captured note to the shared curated zone by default. Set requireExisting to declare an update: the capture then only modifies a note that already exists and needs no conflict approval.',
|
|
210
221
|
parameters: {
|
|
211
222
|
title: {
|
|
212
223
|
type: 'string',
|
|
@@ -246,6 +257,10 @@ export function apply(ctx, config) {
|
|
|
246
257
|
enum: ['ask', 'skip'],
|
|
247
258
|
description: 'Override the deployment conflict policy for this capture: ask for approval on conflict, or skip the write.',
|
|
248
259
|
},
|
|
260
|
+
requireExisting: {
|
|
261
|
+
type: 'boolean',
|
|
262
|
+
description: 'Update-only capture: the note must already exist, and a conflict needs no approval because updating that note is the declared intent. A missing note fails instead of creating one.',
|
|
263
|
+
},
|
|
249
264
|
},
|
|
250
265
|
output: {
|
|
251
266
|
schema: { type: 'json' },
|
|
@@ -256,6 +271,7 @@ export function apply(ctx, config) {
|
|
|
256
271
|
const notesDir = scope === 'shared' ? resolved.notesDir : '';
|
|
257
272
|
const targetId = resolveNoteId(args.id, args.title, scope, notesDir);
|
|
258
273
|
const mode = args.mode ?? 'append';
|
|
274
|
+
const requireExisting = args.requireExisting === true;
|
|
259
275
|
const tags = Array.isArray(args.tags) ? args.tags.map(String) : [];
|
|
260
276
|
const [titleSearch, idSearch] = await Promise.all([
|
|
261
277
|
callTool(ctx, exec, 'wiki_search', { query: args.title }),
|
|
@@ -264,7 +280,11 @@ export function apply(ctx, config) {
|
|
|
264
280
|
const conflicts = dedupeHits(findConflicts(targetId, args.title, [...titleSearch.hits, ...idSearch.hits]));
|
|
265
281
|
const conflictExists = conflicts.length > 0;
|
|
266
282
|
const finalId = conflicts[0]?.id ?? targetId;
|
|
267
|
-
|
|
283
|
+
// A declared update-only capture targets an existing note by
|
|
284
|
+
// construction, so the approval that guards against accidentally
|
|
285
|
+
// clobbering one while creating is not required; `wiki_write` enforces
|
|
286
|
+
// the existence requirement itself, atomically with the write.
|
|
287
|
+
if (conflictExists && !requireExisting) {
|
|
268
288
|
const policy = args.conflictPolicy ?? resolved.conflictPolicy;
|
|
269
289
|
if (policy === 'skip') {
|
|
270
290
|
return {
|
|
@@ -306,9 +326,16 @@ export function apply(ctx, config) {
|
|
|
306
326
|
const now = formatBeijingTime(new Date());
|
|
307
327
|
const detailsText = args.details ? `\n\n## Details\n\n${args.details}` : '';
|
|
308
328
|
const appendContent = `${args.summary}${detailsText}\n`;
|
|
309
|
-
|
|
329
|
+
// A conflict already proves the note exists, so the write appends. An
|
|
330
|
+
// update-only capture without a detected conflict has to ask the vault
|
|
331
|
+
// instead of the search: a note missed by the ranking must be appended
|
|
332
|
+
// to, and one that is genuinely absent fails the write rather than
|
|
333
|
+
// replacing it. The other path stays conflict-driven so an ordinary new
|
|
334
|
+
// capture keeps creating its note.
|
|
335
|
+
const exists = requireExisting ? (await readFrontmatter(ctx, exec, finalId)) !== undefined : conflictExists;
|
|
336
|
+
const appending = exists && mode === 'append';
|
|
310
337
|
const writeMode = appending ? 'append' : 'overwrite';
|
|
311
|
-
ctx.logger?.debug(`memory-curator: capture target=${finalId} conflicts=${conflicts.length} writeMode=${writeMode}`);
|
|
338
|
+
ctx.logger?.debug(`memory-curator: capture target=${finalId} conflicts=${conflicts.length} writeMode=${writeMode} requireExisting=${requireExisting}`);
|
|
312
339
|
const frontmatter = appending ? undefined : await overwriteFrontmatter(ctx, exec, finalId, args.title, tags, now);
|
|
313
340
|
const content = appending
|
|
314
341
|
? appendContent
|
|
@@ -317,12 +344,15 @@ export function apply(ctx, config) {
|
|
|
317
344
|
id: finalId,
|
|
318
345
|
content,
|
|
319
346
|
mode: writeMode,
|
|
347
|
+
...(requireExisting ? { requireExisting: true } : {}),
|
|
320
348
|
});
|
|
321
349
|
const response = {
|
|
322
350
|
written: true,
|
|
323
351
|
id: finalId,
|
|
324
352
|
mode: writeMode,
|
|
325
|
-
conflict:
|
|
353
|
+
// A declared update is not a conflict: the caller named the note it
|
|
354
|
+
// meant to change, so nothing required approval or a skip decision.
|
|
355
|
+
conflict: conflictExists && !requireExisting,
|
|
326
356
|
version: writeResult.version,
|
|
327
357
|
};
|
|
328
358
|
if (typeof writeResult.created === 'string' && writeResult.created !== '')
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jacklika/dsh-memory-curator",
|
|
3
3
|
"description": "Curator tools for the DSH memory vault: memory_recall before acting and memory_capture after significant work, with conflict detection and optional user approval.",
|
|
4
|
-
"version": "0.1.0-alpha.
|
|
4
|
+
"version": "0.1.0-alpha.12",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -44,6 +44,6 @@
|
|
|
44
44
|
"@deepseek-ai/dsh-util-values": ">=0.1.7-rc.1 <0.3.0-0 || >=0.2.0-rc.1 <0.3.0-0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@jacklika/dsh-tool-memory-filesystem": "^0.1.3-alpha.
|
|
47
|
+
"@jacklika/dsh-tool-memory-filesystem": "^0.1.3-alpha.12"
|
|
48
48
|
}
|
|
49
49
|
}
|