@jacklika/dsh-memory-curator 0.1.0-alpha.12 → 0.1.0-alpha.14
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 +27 -23
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Curator tools for the DSH memory vault. This package is a member of the `@jackli
|
|
|
5
5
|
## Tools
|
|
6
6
|
|
|
7
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
|
|
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 targeting the exact derived or explicit `id` (title matches do not redirect it): it replaces the body of an existing note by default (provide the complete current summary), preserves `created` and other frontmatter fields, and fails instead of creating a missing note. Pass `mode: 'append'` explicitly only to add genuinely new material; old text remains visible. Without `requireExisting`, approved conflicts still default to append. 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 updates, 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
|
|
|
@@ -13,6 +13,6 @@ When `id` is omitted, `memory_capture` derives it from the title: Unicode letter
|
|
|
13
13
|
|
|
14
14
|
## Overwriting a capture
|
|
15
15
|
|
|
16
|
-
`mode: 'overwrite'` replaces the note body but merges its frontmatter: fields another writer recorded and the original `created` survive, `title` and `tags` come from this call, and `updated` is stamped. A first capture writes `created` only.
|
|
16
|
+
`mode: 'overwrite'` replaces the note body but merges its frontmatter: fields another writer recorded and the original `created` survive, `title` and `tags` come from this call, and `updated` is stamped. An intervening change detected between reading the old note and writing it fails instead of replacing that change. A first capture writes `created` only.
|
|
17
17
|
|
|
18
18
|
See the Bundle documentation in `docs/usage.md` for configuration and workflow.
|
package/README.zh.md
CHANGED
|
@@ -5,7 +5,7 @@ DSH 记忆库的策展工具。本包是 `@jacklika/dsh-memory` Bundle 的成员
|
|
|
5
5
|
## 工具
|
|
6
6
|
|
|
7
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`
|
|
8
|
+
- `memory_capture(title, summary, ...)` — 在有意义的任务结束后持久化知识,支持冲突检测与可选的用户审批。设置 `requireExisting: true` 表示只更新、不新建,严格使用标题推导或显式给出的目标 `id`(不会重定向到同标题的其他笔记):默认覆盖现有笔记的正文(`summary` 应包含完整的当前结论),保留 `created` 等既有 frontmatter 字段;笔记缺失时直接失败而不会创建。只有新增材料才显式传 `mode: 'append'`,此时旧正文仍可见。不设置 `requireExisting` 时,经批准的冲突写入仍默认追加。由于“更新这篇笔记”本身就是声明的意图,此类调用不会请求冲突审批。返回 `{ 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
|
|
|
@@ -13,6 +13,6 @@ DSH 记忆库的策展工具。本包是 `@jacklika/dsh-memory` Bundle 的成员
|
|
|
13
13
|
|
|
14
14
|
## 覆盖式入库
|
|
15
15
|
|
|
16
|
-
`mode: 'overwrite'` 替换笔记正文,但会合并既有 frontmatter:其他写入方记录的字段与原始 `created` 都保留,`title` 与 `tags` 以本次调用为准,并写入 `updated
|
|
16
|
+
`mode: 'overwrite'` 替换笔记正文,但会合并既有 frontmatter:其他写入方记录的字段与原始 `created` 都保留,`title` 与 `tags` 以本次调用为准,并写入 `updated`。如果从读取旧笔记到写入之间检测到其他修改,则拒绝覆盖该修改。首次入库只写 `created`。
|
|
17
17
|
|
|
18
18
|
配置与工作流请参阅 Bundle 文档 `docs/usage.zh.md`。
|
package/lib/index.js
CHANGED
|
@@ -121,14 +121,17 @@ function dedupeHits(hits) {
|
|
|
121
121
|
* @param title - title from this capture; it wins over any stored one.
|
|
122
122
|
* @param tags - tags from this capture; they win over any stored ones.
|
|
123
123
|
* @param now - Beijing-time timestamp used for `created`/`updated`.
|
|
124
|
-
* @returns the
|
|
124
|
+
* @returns the stored frontmatter and version for a guarded write.
|
|
125
125
|
*/
|
|
126
126
|
async function readFrontmatter(ctx, exec, id) {
|
|
127
127
|
try {
|
|
128
128
|
const note = await callTool(ctx, exec, 'wiki_read', { id });
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
return {
|
|
130
|
+
frontmatter: note !== null && typeof note === 'object' && note.frontmatter !== null && typeof note.frontmatter === 'object'
|
|
131
|
+
? note.frontmatter
|
|
132
|
+
: {},
|
|
133
|
+
version: note.version,
|
|
134
|
+
};
|
|
132
135
|
}
|
|
133
136
|
catch (error) {
|
|
134
137
|
// Only a missing note means a first capture: `wiki_read` reports it as
|
|
@@ -141,12 +144,12 @@ async function readFrontmatter(ctx, exec, id) {
|
|
|
141
144
|
}
|
|
142
145
|
async function overwriteFrontmatter(ctx, exec, id, title, tags, now) {
|
|
143
146
|
const existing = await readFrontmatter(ctx, exec, id);
|
|
144
|
-
const frontmatter = { ...existing, title, tags };
|
|
145
|
-
const created = existing?.created;
|
|
147
|
+
const frontmatter = { ...existing?.frontmatter, title, tags };
|
|
148
|
+
const created = existing?.frontmatter.created;
|
|
146
149
|
frontmatter.created = typeof created === 'string' && created !== '' ? created : now;
|
|
147
150
|
if (existing !== undefined)
|
|
148
151
|
frontmatter.updated = now;
|
|
149
|
-
return frontmatter;
|
|
152
|
+
return { frontmatter, ...(existing !== undefined ? { baseVersion: existing.version } : {}) };
|
|
150
153
|
}
|
|
151
154
|
/**
|
|
152
155
|
* Register `memory_recall` and `memory_capture` on the tool registry.
|
|
@@ -217,7 +220,7 @@ export function apply(ctx, config) {
|
|
|
217
220
|
})));
|
|
218
221
|
ctx.effect(() => ctx.tools.register(defineTool({
|
|
219
222
|
name: 'memory_capture',
|
|
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
|
|
223
|
+
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 replace an existing note with one current version without conflict approval; use mode: append only for genuinely additive material.',
|
|
221
224
|
parameters: {
|
|
222
225
|
title: {
|
|
223
226
|
type: 'string',
|
|
@@ -227,7 +230,7 @@ export function apply(ctx, config) {
|
|
|
227
230
|
summary: {
|
|
228
231
|
type: 'string',
|
|
229
232
|
required: true,
|
|
230
|
-
description: 'The durable knowledge to record
|
|
233
|
+
description: 'The durable knowledge to record in concise Markdown. For a correction using requireExisting, provide the complete current conclusion, not only the changed fragment.',
|
|
231
234
|
},
|
|
232
235
|
details: {
|
|
233
236
|
type: 'string',
|
|
@@ -250,7 +253,7 @@ export function apply(ctx, config) {
|
|
|
250
253
|
mode: {
|
|
251
254
|
type: 'string',
|
|
252
255
|
enum: ['append', 'overwrite'],
|
|
253
|
-
description: '
|
|
256
|
+
description: 'Use overwrite when the new summary supersedes an old conclusion; it replaces the whole body. Use append only for genuinely additive material: it adds a timestamped section and leaves the old text visible. Defaults to overwrite with requireExisting, otherwise append.',
|
|
254
257
|
},
|
|
255
258
|
conflictPolicy: {
|
|
256
259
|
type: 'string',
|
|
@@ -259,7 +262,7 @@ export function apply(ctx, config) {
|
|
|
259
262
|
},
|
|
260
263
|
requireExisting: {
|
|
261
264
|
type: 'boolean',
|
|
262
|
-
description: 'Update-only capture: the note must
|
|
265
|
+
description: 'Update-only capture: the note must exist and needs no conflict approval. Defaults to overwrite with a complete replacement summary; pass mode: append explicitly for additive material. A missing note fails instead of creating one.',
|
|
263
266
|
},
|
|
264
267
|
},
|
|
265
268
|
output: {
|
|
@@ -270,8 +273,8 @@ export function apply(ctx, config) {
|
|
|
270
273
|
const scope = args.scope ?? resolved.defaultScope;
|
|
271
274
|
const notesDir = scope === 'shared' ? resolved.notesDir : '';
|
|
272
275
|
const targetId = resolveNoteId(args.id, args.title, scope, notesDir);
|
|
273
|
-
const mode = args.mode ?? 'append';
|
|
274
276
|
const requireExisting = args.requireExisting === true;
|
|
277
|
+
const mode = args.mode ?? (requireExisting ? 'overwrite' : 'append');
|
|
275
278
|
const tags = Array.isArray(args.tags) ? args.tags.map(String) : [];
|
|
276
279
|
const [titleSearch, idSearch] = await Promise.all([
|
|
277
280
|
callTool(ctx, exec, 'wiki_search', { query: args.title }),
|
|
@@ -279,7 +282,7 @@ export function apply(ctx, config) {
|
|
|
279
282
|
]);
|
|
280
283
|
const conflicts = dedupeHits(findConflicts(targetId, args.title, [...titleSearch.hits, ...idSearch.hits]));
|
|
281
284
|
const conflictExists = conflicts.length > 0;
|
|
282
|
-
const finalId = conflicts[0]?.id ?? targetId;
|
|
285
|
+
const finalId = requireExisting ? targetId : conflicts[0]?.id ?? targetId;
|
|
283
286
|
// A declared update-only capture targets an existing note by
|
|
284
287
|
// construction, so the approval that guards against accidentally
|
|
285
288
|
// clobbering one while creating is not required; `wiki_write` enforces
|
|
@@ -307,9 +310,9 @@ export function apply(ctx, config) {
|
|
|
307
310
|
agent: exec.agent,
|
|
308
311
|
toolName: 'memory_capture',
|
|
309
312
|
callId: exec.callId,
|
|
310
|
-
reason: `memory_capture conflicts with existing note(s):\n${conflictList}\nProposed action: ${mode === 'append' ? 'append a new section to' : '
|
|
313
|
+
reason: `memory_capture conflicts with existing note(s):\n${conflictList}\nProposed action: ${mode === 'append' ? 'append a new section (old text remains visible) to' : 'replace the entire body of'} "${finalId}".`,
|
|
311
314
|
displayReason: {
|
|
312
|
-
en: `Memory capture conflicts with existing note(s):\n${conflictList}\nAllow ${mode} to "${finalId}"
|
|
315
|
+
en: `Memory capture conflicts with existing note(s):\n${conflictList}\nAllow ${mode} to "${finalId}"? ${mode === 'append' ? 'Old conclusions remain visible.' : 'The old body is replaced.'}`,
|
|
313
316
|
},
|
|
314
317
|
signal: exec.signal,
|
|
315
318
|
});
|
|
@@ -326,24 +329,25 @@ export function apply(ctx, config) {
|
|
|
326
329
|
const now = formatBeijingTime(new Date());
|
|
327
330
|
const detailsText = args.details ? `\n\n## Details\n\n${args.details}` : '';
|
|
328
331
|
const appendContent = `${args.summary}${detailsText}\n`;
|
|
329
|
-
// A conflict
|
|
330
|
-
//
|
|
331
|
-
//
|
|
332
|
-
//
|
|
333
|
-
//
|
|
334
|
-
//
|
|
332
|
+
// A conflict suggests the note exists, but update-only writes use the
|
|
333
|
+
// exact target id and ask the vault instead of relying on search ranking.
|
|
334
|
+
// A missing note fails the write; other captures stay conflict-driven
|
|
335
|
+
// and create new notes. Overwrites carry the read version to wiki_write
|
|
336
|
+
// so an intervening change is refused rather than silently replaced.
|
|
337
|
+
// Append remains available when the caller requests additive material.
|
|
335
338
|
const exists = requireExisting ? (await readFrontmatter(ctx, exec, finalId)) !== undefined : conflictExists;
|
|
336
339
|
const appending = exists && mode === 'append';
|
|
337
340
|
const writeMode = appending ? 'append' : 'overwrite';
|
|
338
341
|
ctx.logger?.debug(`memory-curator: capture target=${finalId} conflicts=${conflicts.length} writeMode=${writeMode} requireExisting=${requireExisting}`);
|
|
339
|
-
const
|
|
342
|
+
const overwrite = appending ? undefined : await overwriteFrontmatter(ctx, exec, finalId, args.title, tags, now);
|
|
340
343
|
const content = appending
|
|
341
344
|
? appendContent
|
|
342
|
-
: `---\n${yaml.dump(frontmatter).trim()}\n---\n\n# ${args.title}\n\n${appendContent}`;
|
|
345
|
+
: `---\n${yaml.dump(overwrite?.frontmatter).trim()}\n---\n\n# ${args.title}\n\n${appendContent}`;
|
|
343
346
|
const writeResult = await callTool(ctx, exec, 'wiki_write', {
|
|
344
347
|
id: finalId,
|
|
345
348
|
content,
|
|
346
349
|
mode: writeMode,
|
|
350
|
+
...(overwrite?.baseVersion !== undefined ? { baseVersion: overwrite.baseVersion } : {}),
|
|
347
351
|
...(requireExisting ? { requireExisting: true } : {}),
|
|
348
352
|
});
|
|
349
353
|
const response = {
|
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.14",
|
|
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.13"
|
|
48
48
|
}
|
|
49
49
|
}
|