@jacklika/dsh-memory-curator 0.1.0-alpha.11 → 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 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. 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.
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
@@ -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, ...)` — 在有意义的任务结束后持久化知识,支持冲突检测与可选的用户审批。返回 `{ written, id, mode, conflict, version, created?, updated?, committed? }`:`version` 是持久化笔记文本的 SHA-1 指纹;首次入库返回 `created`;追加写入返回 `updated`;挂载 `memory-git` 完成提交时还会返回 `committed`。
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 overwriteFrontmatter(ctx, exec, id, title, tags, now) {
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
- if (note !== null && typeof note === 'object' && note.frontmatter !== null && typeof note.frontmatter === 'object') {
131
- existing = note.frontmatter;
132
- }
129
+ return note !== null && typeof note === 'object' && note.frontmatter !== null && typeof note.frontmatter === 'object'
130
+ ? note.frontmatter
131
+ : {};
133
132
  }
134
- catch {
135
- // No readable note at this id: the ordinary first capture of a new note.
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;
@@ -210,7 +217,7 @@ export function apply(ctx, config) {
210
217
  })));
211
218
  ctx.effect(() => ctx.tools.register(defineTool({
212
219
  name: 'memory_capture',
213
- 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.',
214
221
  parameters: {
215
222
  title: {
216
223
  type: 'string',
@@ -250,6 +257,10 @@ export function apply(ctx, config) {
250
257
  enum: ['ask', 'skip'],
251
258
  description: 'Override the deployment conflict policy for this capture: ask for approval on conflict, or skip the write.',
252
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
+ },
253
264
  },
254
265
  output: {
255
266
  schema: { type: 'json' },
@@ -260,6 +271,7 @@ export function apply(ctx, config) {
260
271
  const notesDir = scope === 'shared' ? resolved.notesDir : '';
261
272
  const targetId = resolveNoteId(args.id, args.title, scope, notesDir);
262
273
  const mode = args.mode ?? 'append';
274
+ const requireExisting = args.requireExisting === true;
263
275
  const tags = Array.isArray(args.tags) ? args.tags.map(String) : [];
264
276
  const [titleSearch, idSearch] = await Promise.all([
265
277
  callTool(ctx, exec, 'wiki_search', { query: args.title }),
@@ -268,7 +280,11 @@ export function apply(ctx, config) {
268
280
  const conflicts = dedupeHits(findConflicts(targetId, args.title, [...titleSearch.hits, ...idSearch.hits]));
269
281
  const conflictExists = conflicts.length > 0;
270
282
  const finalId = conflicts[0]?.id ?? targetId;
271
- if (conflictExists) {
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) {
272
288
  const policy = args.conflictPolicy ?? resolved.conflictPolicy;
273
289
  if (policy === 'skip') {
274
290
  return {
@@ -310,9 +326,16 @@ export function apply(ctx, config) {
310
326
  const now = formatBeijingTime(new Date());
311
327
  const detailsText = args.details ? `\n\n## Details\n\n${args.details}` : '';
312
328
  const appendContent = `${args.summary}${detailsText}\n`;
313
- const appending = conflictExists && mode === 'append';
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';
314
337
  const writeMode = appending ? 'append' : 'overwrite';
315
- 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}`);
316
339
  const frontmatter = appending ? undefined : await overwriteFrontmatter(ctx, exec, finalId, args.title, tags, now);
317
340
  const content = appending
318
341
  ? appendContent
@@ -321,12 +344,15 @@ export function apply(ctx, config) {
321
344
  id: finalId,
322
345
  content,
323
346
  mode: writeMode,
347
+ ...(requireExisting ? { requireExisting: true } : {}),
324
348
  });
325
349
  const response = {
326
350
  written: true,
327
351
  id: finalId,
328
352
  mode: writeMode,
329
- conflict: conflictExists,
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,
330
356
  version: writeResult.version,
331
357
  };
332
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.11",
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.10"
47
+ "@jacklika/dsh-tool-memory-filesystem": "^0.1.3-alpha.12"
48
48
  }
49
49
  }