@yeaft/webchat-agent 1.0.349 → 1.0.350

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.
Files changed (36) hide show
  1. package/local-runtime/version.json +1 -1
  2. package/package.json +1 -1
  3. package/yeaft/dream/apply.js +59 -30
  4. package/yeaft/dream/output-snapshot.js +8 -4
  5. package/yeaft/dream/prompts/consolidate-topics.md +31 -0
  6. package/yeaft/dream/prompts/create.md +8 -8
  7. package/yeaft/dream/prompts/index.js +4 -2
  8. package/yeaft/dream/prompts/merge-topics.md +35 -0
  9. package/yeaft/dream/prompts/triage-pass1.md +2 -2
  10. package/yeaft/dream/prompts/triage-pass2.md +4 -2
  11. package/yeaft/dream/prompts/update.md +16 -14
  12. package/yeaft/dream/runner.js +69 -13
  13. package/yeaft/dream/segment-extract.js +16 -13
  14. package/yeaft/dream/session-wiring.js +2 -2
  15. package/yeaft/dream/snapshot.js +3 -3
  16. package/yeaft/dream/topic-consolidation.js +316 -0
  17. package/yeaft/dream/triage.js +7 -2
  18. package/yeaft/engine.js +221 -236
  19. package/yeaft/memory/ams-registry.js +42 -61
  20. package/yeaft/memory/ams.js +17 -9
  21. package/yeaft/memory/budget.js +15 -18
  22. package/yeaft/memory/content-backfill.js +118 -0
  23. package/yeaft/memory/index-db.js +10 -3
  24. package/yeaft/memory/keywords.js +25 -7
  25. package/yeaft/memory/preflow.js +26 -9
  26. package/yeaft/memory/segment-store.js +44 -8
  27. package/yeaft/memory/segment-sync.js +8 -4
  28. package/yeaft/memory/segment.js +10 -3
  29. package/yeaft/memory/store.js +88 -38
  30. package/yeaft/memory/summary-store.js +3 -3
  31. package/yeaft/memory/topic-redirect.js +28 -0
  32. package/yeaft/session.js +18 -19
  33. package/yeaft/sessions/pre-flow.js +7 -3
  34. package/yeaft/sub-agent/runner.js +10 -1
  35. package/yeaft/work-center/bridge.js +1 -0
  36. package/yeaft/work-center/runner.js +33 -4
@@ -1 +1 @@
1
- {"version":"1.0.349"}
1
+ {"version":"1.0.350"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yeaft/webchat-agent",
3
- "version": "1.0.349",
3
+ "version": "1.0.350",
4
4
  "description": "Remote worker agent for Yeaft Web Code Agent — connects the native Yeaft engine, CLI providers, and workbench tools",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -2,17 +2,18 @@
2
2
  * dream/apply.js.
3
3
  *
4
4
  * Execute one merged-target action: either UPDATE an existing scope's
5
- * memory.md/summary.md, or CREATE a new one (currently topic-only).
5
+ * canonical content.md/summary.md, or CREATE a new one (currently topic-only).
6
+ * Atomic evidence segments remain in memory.md and are maintained separately.
6
7
  *
7
8
  * The flow per target:
8
9
  * 1. snapshot existing files into .dream-bak/<ts>/<scope>/
9
10
  * 2. if total content fits, run UPDATE (or CREATE) once
10
11
  * 3. if it doesn't, batch the sources by group (segment.batchSourcesForApply)
11
12
  * and chain the LLM calls — each batch's output becomes the next
12
- * batch's `current memory.md`. The prompt threads "this is batch K
13
+ * batch's current content.md. The prompt threads "this is batch K
13
14
  * of N" so the LLM doesn't think previous content was lost.
14
- * 4. write tmp + rename for memory.md and summary.md
15
- * 5. update the per-scope dream-state marker inside memory.md
15
+ * 4. atomically write content.md and summary.md
16
+ * 5. sync canonical content into the derived FTS scope index
16
17
  *
17
18
  * The LLM call is injected (`opts.llm`). Snapshots are injectable too
18
19
  * (`opts.snapshot`) to allow tests to skip the .dream-bak side-effect.
@@ -22,8 +23,9 @@ import { promises as fsp } from 'fs';
22
23
  import { join, dirname } from 'path';
23
24
  import { inspect } from 'util';
24
25
 
25
- import { writeMemory, writeSummary, readMemory, readSummary } from '../memory/store.js';
26
- import { withDreamMarker } from './state.js';
26
+ import { writeContent, writeSummary, readContent, readMemory, readSummary } from '../memory/store.js';
27
+ import { parseSegments } from '../memory/segment.js';
28
+ import { syncScope } from '../memory/segment-sync.js';
27
29
  import { batchSourcesForApply, needsBatchedApply, truncateMessage } from './segment.js';
28
30
  import { snapshotScope } from './snapshot.js';
29
31
  import { parseJsonSafe } from './triage.js';
@@ -43,8 +45,8 @@ function rawResponseSnippet(raw) {
43
45
 
44
46
  function applySystem(language) {
45
47
  return String(language || '').toLowerCase().startsWith('zh')
46
- ? '你是梦境流水线的 Apply 阶段。你会根据最近的 session 对话重写单个 scope 的 memory.md summary.md。请只回复严格 JSON,不要输出说明文字或 markdown fence。memory_md 和 summary_md 的自然语言内容必须使用中文;JSON key、scope、schema 字段和代码标识符保持英文。'
47
- : 'You are the Apply stage of a dream pipeline. You rewrite a single scope\'s memory.md and summary.md based on recent session conversations. Reply with strict JSON only — no prose, no fences.';
48
+ ? '你是梦境流水线的 Apply 阶段。你会根据最近的 session 对话重写单个 scope 的 canonical content.md 和目录 summary.md。请只回复严格 JSON,不要输出说明文字或 markdown fence。content_md 和 summary_md 的自然语言内容必须使用中文;JSON key、scope、schema 字段和代码标识符保持英文。'
49
+ : 'You are the Apply stage of a dream pipeline. You rewrite a single scope\'s canonical content.md and catalog summary.md based on recent session conversations. Reply with strict JSON only — no prose, no fences.';
48
50
  }
49
51
 
50
52
  function isPrimarySessionTarget(target) {
@@ -114,23 +116,25 @@ function summaryFromLines(lines, zh) {
114
116
  *
115
117
  * @param {{
116
118
  * target: string,
117
- * memoryMd: string,
119
+ * contentMd?: string,
120
+ * memoryMd?: string,
118
121
  * summaryMd: string,
119
122
  * sources: Array<{ sessionId: string, diff: Array<object> }>,
120
123
  * batchInfo?: { index: number, total: number },
121
124
  * }} ctx
122
125
  */
123
126
  export function buildUpdatePrompt(ctx) {
127
+ const contentMd = typeof ctx.contentMd === 'string' ? ctx.contentMd : (ctx.memoryMd || '');
124
128
  const batchHeader = (ctx.batchInfo && ctx.batchInfo.total > 1)
125
129
  ? (String(ctx.language || '').toLowerCase().startsWith('zh')
126
- ? `这是第 ${ctx.batchInfo.index}/${ctx.batchInfo.total} 批。前面的批次已经合并进下面当前的 memory.md。\n`
127
- : `This is batch ${ctx.batchInfo.index} of ${ctx.batchInfo.total}.\nEarlier batches have already been folded into the current memory.md below.\n`)
130
+ ? `这是第 ${ctx.batchInfo.index}/${ctx.batchInfo.total} 批。前面的批次已经合并进下面当前的 content.md。\n`
131
+ : `This is batch ${ctx.batchInfo.index} of ${ctx.batchInfo.total}.\nEarlier batches have already been folded into the current content.md below.\n`)
128
132
  : '';
129
133
  const sources = renderSourceBlocks(ctx.sources, ctx.language);
130
134
  return render('update', {
131
135
  target: ctx.target,
132
136
  batchHeader,
133
- memoryMd: ctx.memoryMd || '',
137
+ contentMd,
134
138
  summaryMd: ctx.summaryMd || '',
135
139
  sources,
136
140
  }, { language: ctx.language });
@@ -240,6 +244,7 @@ export function targetToScope(target) {
240
244
  * nowIso?: () => string,
241
245
  * onProgress?: (event: object) => void,
242
246
  * siblingTopicsFor?: (target: string) => Promise<Array<{path:string, summary:string}>>,
247
+ * segmentIndex?: import('../memory/index-db.js').SegmentIndex|null,
243
248
  * }} opts
244
249
  */
245
250
  export async function applyMergedTarget(merged, opts) {
@@ -247,17 +252,20 @@ export async function applyMergedTarget(merged, opts) {
247
252
  if (!opts.llm) throw new Error('apply.applyMergedTarget: opts.llm required');
248
253
  const ts = opts.ts || new Date().toISOString().replace(/[:.]/g, '-');
249
254
  const snapFn = opts.snapshot || snapshotScope;
250
- const nowIso = opts.nowIso ? opts.nowIso() : new Date().toISOString();
251
255
  const scope = targetToScope(merged.target);
252
256
  const scopeDirRel = scopeRelDir(scope);
253
257
 
254
258
  if (opts.onProgress) opts.onProgress({ phase: 'apply', target: merged.target, status: 'snapshot' });
255
259
  await snapFn(opts.root, ts, scopeDirRel);
256
260
 
257
- let memoryMd = await readMemory(scope, { root: opts.root });
261
+ let contentMd = await readContent(scope, { root: opts.root });
262
+ const legacyMemoryMd = contentMd ? '' : await readMemory(scope, { root: opts.root });
263
+ if (!contentMd && isLegacyCanonicalMemory(legacyMemoryMd, merged.target)) {
264
+ contentMd = legacyMemoryMd;
265
+ }
258
266
  let summaryMd = await readSummary(scope, { root: opts.root, language: opts.language });
259
267
 
260
- if (merged.kind === 'create' && (memoryMd || summaryMd)) {
268
+ if (merged.kind === 'create' && (contentMd || summaryMd)) {
261
269
  // Race / partial state: the scope already exists. Treat as update —
262
270
  // safer than overwriting arbitrary bytes.
263
271
  merged = { ...merged, kind: 'update' };
@@ -277,26 +285,29 @@ export async function applyMergedTarget(merged, opts) {
277
285
  if (opts.onProgress) opts.onProgress({ phase: 'apply', target: merged.target, status: 'llm', batch: 1, of: 1 });
278
286
  const raw = await opts.llm({ pass: 'create', prompt, system: applySystem(opts.language) });
279
287
  const parsed = parseJsonSafe(raw);
280
- if (!parsed || typeof parsed.memory_md !== 'string') {
288
+ const parsedContent = parsed && typeof parsed.content_md === 'string'
289
+ ? parsed.content_md
290
+ : (parsed && typeof parsed.memory_md === 'string' ? parsed.memory_md : null);
291
+ if (parsedContent === null) {
281
292
  throw malformedJsonError(`apply: CREATE returned malformed JSON for ${merged.target}`, raw);
282
293
  }
283
294
  const ensured = ensurePrimarySessionOutput({
284
295
  target: merged.target,
285
- memoryMd: parsed.memory_md,
296
+ memoryMd: parsedContent,
286
297
  summaryMd: typeof parsed.summary_md === 'string' ? parsed.summary_md : '',
287
298
  sources: merged.sources,
288
299
  language: opts.language,
289
300
  });
290
- memoryMd = ensured.memoryMd;
301
+ contentMd = ensured.memoryMd;
291
302
  summaryMd = ensured.summaryMd;
292
303
  batchesUsed = 1;
293
304
  } else {
294
305
  // UPDATE — possibly batched.
295
306
  const batches = needsBatchedApply(
296
- { memoryMd, summaryMd, sources: merged.sources },
307
+ { memoryMd: contentMd, summaryMd, sources: merged.sources },
297
308
  maxApply,
298
309
  )
299
- ? batchSourcesForApply({ memoryMd, summaryMd, sources: merged.sources }, maxApply)
310
+ ? batchSourcesForApply({ memoryMd: contentMd, summaryMd, sources: merged.sources }, maxApply)
300
311
  : [merged.sources];
301
312
 
302
313
  let i = 0;
@@ -304,7 +315,7 @@ export async function applyMergedTarget(merged, opts) {
304
315
  i += 1;
305
316
  const prompt = buildUpdatePrompt({
306
317
  target: merged.target,
307
- memoryMd,
318
+ contentMd,
308
319
  summaryMd,
309
320
  sources: batch,
310
321
  batchInfo: { index: i, total: batches.length },
@@ -313,26 +324,32 @@ export async function applyMergedTarget(merged, opts) {
313
324
  if (opts.onProgress) opts.onProgress({ phase: 'apply', target: merged.target, status: 'llm', batch: i, of: batches.length });
314
325
  const raw = await opts.llm({ pass: 'update', prompt, system: applySystem(opts.language) });
315
326
  const parsed = parseJsonSafe(raw);
316
- if (!parsed || typeof parsed.memory_md !== 'string') {
327
+ const parsedContent = parsed && typeof parsed.content_md === 'string'
328
+ ? parsed.content_md
329
+ : (parsed && typeof parsed.memory_md === 'string' ? parsed.memory_md : null);
330
+ if (parsedContent === null) {
317
331
  throw malformedJsonError(`apply: UPDATE batch ${i} returned malformed JSON for ${merged.target}`, raw);
318
332
  }
319
333
  const ensured = ensurePrimarySessionOutput({
320
334
  target: merged.target,
321
- memoryMd: parsed.memory_md,
335
+ memoryMd: parsedContent,
322
336
  summaryMd: typeof parsed.summary_md === 'string' ? parsed.summary_md : summaryMd,
323
337
  sources: batch,
324
338
  language: opts.language,
325
339
  });
326
- memoryMd = ensured.memoryMd;
340
+ contentMd = ensured.memoryMd;
327
341
  summaryMd = ensured.summaryMd;
328
342
  }
329
343
  batchesUsed = batches.length;
330
344
  }
331
345
 
332
- // Stamp the per-scope dream marker, then atomically write both files.
333
- const stamped = withDreamMarker(memoryMd, { lastDreamAt: nowIso });
334
- await writeMemory(scope, stamped, { root: opts.root });
346
+ // Canonical content and the catalog summary are independent atomic files.
347
+ // Per-session Dream control state is stored in .dream-state by runner.js;
348
+ // do not mix scheduler metadata into prompt-facing content.
349
+ const canonicalContent = String(contentMd || '').trim();
350
+ await writeContent(scope, canonicalContent ? `${canonicalContent}\n` : '', { root: opts.root });
335
351
  await writeSummary(scope, summaryMd || '', { root: opts.root, language: opts.language });
352
+ if (opts.segmentIndex) syncScope(opts.root, opts.segmentIndex, merged.target);
336
353
 
337
354
  if (opts.onProgress) {
338
355
  // feat-dream-debug-detail: surface a truncated copy of what was
@@ -345,9 +362,12 @@ export async function applyMergedTarget(merged, opts) {
345
362
  status: 'done',
346
363
  batches: batchesUsed,
347
364
  kind: merged.kind,
348
- memoryMdPreview: truncateForDebug(stamped),
365
+ contentMdPreview: truncateForDebug(canonicalContent),
366
+ contentMdLength: canonicalContent.length,
367
+ // Legacy debug aliases kept until the Dream panel consumes contentMd*.
368
+ memoryMdPreview: truncateForDebug(canonicalContent),
349
369
  summaryMdPreview: truncateForDebug(summaryMd || ''),
350
- memoryMdLength: (stamped || '').length,
370
+ memoryMdLength: canonicalContent.length,
351
371
  summaryMdLength: (summaryMd || '').length,
352
372
  });
353
373
  }
@@ -372,12 +392,21 @@ function scopeRelDir(scope) {
372
392
 
373
393
  function oneLine(s) { return String(s || '').replace(/\s+/g, ' ').trim().slice(0, 200); }
374
394
 
395
+ function isLegacyCanonicalMemory(memoryMd, scope) {
396
+ const raw = String(memoryMd || '').trim();
397
+ if (!raw) return false;
398
+ const parsed = parseSegments(raw, { defaultScope: scope });
399
+ // Body-only legacy prose parses as one anonymous segment. Frontmatter-backed
400
+ // records are evidence and must never be promoted into canonical content.
401
+ return parsed.length === 1 && !/^---\s*$/m.test(raw);
402
+ }
403
+
375
404
  /**
376
405
  * Per-field truncation cap for debug previews emitted on `apply/done`.
377
406
  * Keep this small — the dream panel only needs a recognisable snippet.
378
407
  * Total worst-case payload is `PREVIEW_MAX * 2 * targets_per_run` per
379
408
  * dream pass; with N=50 targets that's ~200 KB. The full bytes are on
380
- * disk under <root>/<scope>/{memory,summary}.md anyway — these previews
409
+ * disk under <root>/<scope>/{content,summary}.md anyway — these previews
381
410
  * are for at-a-glance debugging only.
382
411
  */
383
412
  const PREVIEW_MAX = 2048;
@@ -3,13 +3,13 @@
3
3
  *
4
4
  * Read-only projection of Dream-owned output files for UI observability.
5
5
  * The Dream write path remains `runner -> apply -> memory/store`; callers use
6
- * this module to load the current `memory.md` / `summary.md` contents for a
6
+ * this module to load current `content.md` / `memory.md` / `summary.md` for a
7
7
  * single Yeaft session when emitting status or restoring a switched session.
8
8
  */
9
9
 
10
10
  import { join } from 'node:path';
11
11
 
12
- import { readMemory, readSummary } from '../memory/store.js';
12
+ import { readContent, readMemory, readSummary } from '../memory/store.js';
13
13
  import { readDreamError, readSessionState } from './state.js';
14
14
  import { buildRunDreamOpts } from './session-wiring.js';
15
15
 
@@ -33,7 +33,8 @@ export async function buildDreamOutputSnapshot(sessionLike, sessionId) {
33
33
  const scope = `sessions/${sessionId}`;
34
34
  const memoryScope = { kind: 'session', id: sessionId };
35
35
  const root = join(sessionLike.yeaftDir, 'memory');
36
- const [memoryRaw, summaryRaw, state, lastError] = await Promise.all([
36
+ const [contentRaw, memoryRaw, summaryRaw, state, lastError] = await Promise.all([
37
+ readContent(memoryScope, { root }).catch(() => ''),
37
38
  readMemory(memoryScope, { root }).catch(() => ''),
38
39
  readSummary(memoryScope, { root }).catch(() => ''),
39
40
  readSessionState(root, sessionId).catch(() => ({
@@ -44,6 +45,7 @@ export async function buildDreamOutputSnapshot(sessionLike, sessionId) {
44
45
  readDreamError(root, scope).catch(() => null),
45
46
  ]);
46
47
  const totalMessageCount = await countSessionMessages(sessionLike, sessionId);
48
+ const content = truncateDreamText(contentRaw);
47
49
  const memory = truncateDreamText(memoryRaw);
48
50
  const summary = truncateDreamText(summaryRaw);
49
51
  return {
@@ -54,8 +56,10 @@ export async function buildDreamOutputSnapshot(sessionLike, sessionId) {
54
56
  lastDreamMessageId: state?.lastDreamMessageId || null,
55
57
  messageCount: Number.isFinite(state?.messageCount) ? state.messageCount : 0,
56
58
  totalMessageCount,
57
- hasOutput: !!(memoryRaw || summaryRaw),
59
+ hasOutput: !!(contentRaw || memoryRaw || summaryRaw),
58
60
  lastError,
61
+ contentText: content.text,
62
+ contentTruncated: content.truncated,
59
63
  memoryText: memory.text,
60
64
  memoryTruncated: memory.truncated,
61
65
  summaryText: summary.text,
@@ -0,0 +1,31 @@
1
+ <!-- lang:en -->
2
+ Identify duplicate semantic topics in this catalog.
3
+
4
+ Topics:
5
+ {{topics}}
6
+
7
+ Rules:
8
+ - Merge only topics about the same durable subject whose facts belong in one canonical content document.
9
+ - Different spelling, casing, language, PR number, release, review round, or transient status does not make a distinct topic.
10
+ - Do not merge merely related subjects when either would lose its independent meaning.
11
+ - Prefer a stable, general existing path as canonical; never invent a new path here.
12
+ - Omit uncertain groups.
13
+
14
+ Reply with strict JSON:
15
+ { "groups": [{ "canonical": "<existing path>", "merge": ["<existing path>"] }] }
16
+
17
+ <!-- lang:zh -->
18
+ 识别目录中语义重复的 topics。
19
+
20
+ Topics:
21
+ {{topics}}
22
+
23
+ 规则:
24
+ - 只有主题是同一持久对象、全部事实适合放入同一份 canonical content 时才合并。
25
+ - 拼写、大小写、语言、PR 编号、release、review 轮次或短期状态不同,不构成独立 topic。
26
+ - 仅仅相关但仍有独立含义的主题不要合并。
27
+ - canonical 必须选已有且稳定、通用的路径;这里不得创建新路径。
28
+ - 不确定的组不要输出。
29
+
30
+ 只回复严格 JSON:
31
+ { "groups": [{ "canonical": "<已有路径>", "merge": ["<已有路径>"] }] }
@@ -8,13 +8,13 @@ Source session conversations:
8
8
 
9
9
  {{siblingsBlock}}
10
10
  Task:
11
- 1. Write memory.md from scratch with a reasonable section structure.
12
- 2. Prioritize reusable experience: workflows, preferences, pitfalls, corrections, project conventions, review/merge/tag lessons, and rules that should change future execution.
11
+ 1. Write canonical content.md from scratch with a reasonable section structure.
12
+ 2. Preserve every concrete fact, decision, constraint, preference, workflow, pitfall, correction, project convention, and actionable state relevant to this scope. Do not replace specifics with a vague summary.
13
13
  3. Keep current PR/review/blocker detail only when it is still actionable.
14
- 4. Write summary.md (1–3 sentences) with reusable lessons before current state.
14
+ 4. Write summary.md as a 1–3 sentence catalog description used only for later topic selection.
15
15
 
16
16
  Reply with strict JSON of the shape:
17
- { "memory_md": "...", "summary_md": "..." }
17
+ { "content_md": "...", "summary_md": "..." }
18
18
 
19
19
  <!-- lang:zh -->
20
20
  你正在从零创建一个新的 memory scope。
@@ -26,10 +26,10 @@ Scope path: {{target}} (must be ≤2 levels)
26
26
 
27
27
  {{siblingsBlock}}
28
28
  任务:
29
- 1. 从零编写 memory.md,并使用合理的章节结构。
30
- 2. 优先保留可复用经验:工作流、偏好、坑点、纠偏、项目约定、review/merge/tag 教训,以及会改变后续执行方式的规则。
29
+ 1. 从零编写 canonical content.md,并使用合理的章节结构。
30
+ 2. 保留属于当前 scope 的每条具体事实、决策、约束、偏好、工作流、坑点、纠偏、项目约定和可执行状态;不得用模糊摘要替代具体信息。
31
31
  3. 当前 PR、review、阻塞细节只有仍可执行时才保留。
32
- 4. 编写 summary.md(1–3 句),先写可复用经验,再写当前状态。
32
+ 4. 编写 1–3 句的 summary.md 目录描述,它只用于后续 topic 选择。
33
33
 
34
34
  只回复严格 JSON,结构如下:
35
- { "memory_md": "...", "summary_md": "..." }
35
+ { "content_md": "...", "summary_md": "..." }
@@ -24,6 +24,8 @@ const FILES = {
24
24
  extractTopic: 'extract-topic.md',
25
25
  // H2.e — per-scope summary compression
26
26
  summarizeScope: 'summarize-scope.md',
27
+ consolidateTopics: 'consolidate-topics.md',
28
+ mergeTopics: 'merge-topics.md',
27
29
  };
28
30
 
29
31
  /**
@@ -64,8 +66,8 @@ export function normalizePromptLanguage(language) {
64
66
 
65
67
  function languageDirective(language) {
66
68
  return normalizePromptLanguage(language) === 'zh'
67
- ? '语言要求:请用中文生成所有自然语言内容,尤其是 memory_md 和 summary_md;工具名、scope、JSON key、schema 字段、代码标识符和枚举值必须保持英文。严格按要求输出 JSON 时,不要翻译 JSON key。'
68
- : 'Language requirement: write all natural-language memory content in English. Keep tool names, scopes, JSON keys, schema fields, code identifiers, and enum values in English. When strict JSON is required, do not rename JSON keys.';
69
+ ? '语言要求:请用中文生成所有自然语言内容,尤其是 content_md 和 summary_md;工具名、scope、JSON key、schema 字段、代码标识符和枚举值必须保持英文。严格按要求输出 JSON 时,不要翻译 JSON key。'
70
+ : 'Language requirement: write all natural-language memory content in English, especially content_md and summary_md. Keep tool names, scopes, JSON keys, schema fields, code identifiers, and enum values in English. When strict JSON is required, do not rename JSON keys.';
69
71
  }
70
72
 
71
73
  function load(name) {
@@ -0,0 +1,35 @@
1
+ <!-- lang:en -->
2
+ Merge duplicate topic contents into one canonical topic.
3
+
4
+ Canonical path: {{canonical}}
5
+
6
+ Existing topic contents:
7
+ {{topicContents}}
8
+
9
+ Requirements:
10
+ - Preserve every unique, still-valid concrete fact, decision, constraint, preference, workflow, pitfall, correction, and actionable state.
11
+ - Resolve direct contradictions in favor of the newest explicit correction when evident; otherwise preserve both with an uncertainty note.
12
+ - Remove duplication, stale superseded execution state, and scope-path boilerplate.
13
+ - content_md is authoritative and may be as long as required for information completeness.
14
+ - summary_md is only a 1–3 sentence catalog description.
15
+
16
+ Reply with strict JSON:
17
+ { "content_md": "...", "summary_md": "..." }
18
+
19
+ <!-- lang:zh -->
20
+ 把重复 topic 的内容合并到一个 canonical topic。
21
+
22
+ Canonical 路径:{{canonical}}
23
+
24
+ 现有 topic 内容:
25
+ {{topicContents}}
26
+
27
+ 要求:
28
+ - 保留每一条独有且仍有效的具体事实、决策、约束、偏好、工作流、坑点、纠偏和可执行状态。
29
+ - 如果能明确判断,以最新的显式纠偏解决直接冲突;否则保留双方并标注不确定性。
30
+ - 删除重复、已被推翻的短期执行状态和 scope 路径套话。
31
+ - content_md 是权威正文,可按信息完整性需要保留长度。
32
+ - summary_md 只是 1–3 句的目录描述。
33
+
34
+ 只回复严格 JSON:
35
+ { "content_md": "...", "summary_md": "..." }
@@ -1,7 +1,7 @@
1
1
  <!-- lang:en -->
2
2
  You are deciding whether a recent session conversation carries:
3
3
  - signals that should update the USER profile, and/or
4
- - signals that should update one or more TOPIC scopes.
4
+ - durable signals that should update one or more existing semantic TOPIC scopes.
5
5
 
6
6
  Do NOT mention vp/, session/, feature/, or topic/ scopes — those are handled by hard rules.
7
7
 
@@ -23,7 +23,7 @@ Respond with strict JSON of the shape:
23
23
  <!-- lang:zh -->
24
24
  你要判断最近一段会话对话是否包含:
25
25
  - 应更新用户画像的信号;和/或
26
- - 应更新一个或多个主题作用域的信号。
26
+ - 应更新一个或多个已有语义主题作用域的持久信号。
27
27
 
28
28
  不要提及 `vp/`、`session/`、`feature/` 或 `topic/` 作用域 —— 这些由硬规则处理。
29
29
 
@@ -3,7 +3,8 @@ Bind a free-form topic description to an exact path under topic/.
3
3
  Rules:
4
4
  - At most TWO path segments. Reject any third level.
5
5
  - Segments may contain letters, digits, dashes, underscores, dots, CJK.
6
- - Prefer matching an existing path if the description fits.
6
+ - Existing paths are canonical. Return "match" whenever any existing topic can contain the new information, even if wording, language, casing, a PR number, or a one-off execution state differs.
7
+ - Return "new" only for a stable subject that is materially distinct from every existing topic. Never create a topic merely for one PR, review run, commit, release, status, or spelling variant.
7
8
 
8
9
  Description: {{description}}
9
10
 
@@ -20,7 +21,8 @@ Reply with strict JSON, exactly one of:
20
21
  规则:
21
22
  - 最多 TWO 个路径段。拒绝任何第三级路径。
22
23
  - 路径段可包含字母、数字、短横线、下划线、点、CJK 字符。
23
- - 如果描述适合已有路径,优先匹配已有路径。
24
+ - 已有路径是 canonical。只要任一已有 topic 能容纳新信息,即使措辞、语言、大小写、PR 编号或一次性执行状态不同,也必须返回 "match"。
25
+ - 只有主题稳定且与所有已有 topic 实质不同才返回 "new"。不得只因单个 PR、review 轮次、commit、release、状态或拼写变体创建 topic。
24
26
 
25
27
  描述:{{description}}
26
28
 
@@ -3,12 +3,12 @@ You are updating an existing memory scope.
3
3
 
4
4
  Scope: {{target}}
5
5
  {{batchHeader}}
6
- Current memory.md:
6
+ Current canonical content.md:
7
7
  """
8
- {{memoryMd}}
8
+ {{contentMd}}
9
9
  """
10
10
 
11
- Current summary.md:
11
+ Current catalog summary.md:
12
12
  """
13
13
  {{summaryMd}}
14
14
  """
@@ -20,9 +20,10 @@ Task:
20
20
  - Extract from these conversations what is relevant to THIS scope.
21
21
  - Prefer reusable experience over chronology: workflows, preferences, pitfalls, corrections, project conventions, review/merge/tag lessons, and rules that should change future execution.
22
22
  - Keep current PR/review/blocker detail only when it is still actionable.
23
- - Integrate it into memory.md (reorganize sections if needed).
24
- - Drop stale or contradicted entries.
25
- - Rewrite summary.md (1–3 sentences) so the summary highlights reusable lessons first, then current state.
23
+ - Rewrite content.md as the canonical current text (reorganize sections if needed).
24
+ - Preserve every still-valid concrete fact, decision, constraint, preference, workflow, pitfall, correction, and actionable state from the existing content or new conversations. Do not shorten by dropping unique valid information.
25
+ - Drop only stale, contradicted, duplicated, or scope-irrelevant entries.
26
+ - Rewrite summary.md as a 1–3 sentence catalog description for topic selection. It is not the authoritative memory and must not be used to compensate for omissions from content.md.
26
27
  - The same conversations are being processed for OTHER scopes too.
27
28
  Only handle what is relevant here. Ignore the rest.
28
29
 
@@ -33,19 +34,19 @@ Hard rules:
33
34
  "⚠️ contradicts charter — verify which is current" and continue.
34
35
 
35
36
  Reply with strict JSON of the shape:
36
- { "memory_md": "...", "summary_md": "..." }
37
+ { "content_md": "...", "summary_md": "..." }
37
38
 
38
39
  <!-- lang:zh -->
39
40
  你正在更新一个已有的 memory scope。
40
41
 
41
42
  Scope: {{target}}
42
43
  {{batchHeader}}
43
- 当前 memory.md:
44
+ 当前 canonical content.md:
44
45
  """
45
- {{memoryMd}}
46
+ {{contentMd}}
46
47
  """
47
48
 
48
- 当前 summary.md:
49
+ 当前目录 summary.md:
49
50
  """
50
51
  {{summaryMd}}
51
52
  """
@@ -57,9 +58,10 @@ Scope: {{target}}
57
58
  - 从这些对话中提取与当前作用域相关的内容。
58
59
  - 优先保留可复用经验,而不是流水账:工作流、偏好、坑点、纠偏、项目约定、review/merge/tag 教训,以及会改变后续执行方式的规则。
59
60
  - 当前 PR、review、阻塞细节只有仍可执行时才保留。
60
- - 将内容整合进 memory.md(必要时重组章节)。
61
- - 删除过期或被推翻的条目。
62
- - 重写 summary.md(1–3 句),摘要先突出可复用经验,再写当前状态。
61
+ - content.md 重写为 canonical 当前正文(必要时重组章节)。
62
+ - 对现有正文和新对话中的每条仍有效具体事实、决策、约束、偏好、工作流、坑点、纠偏和可执行状态都必须保留;不得为了缩短篇幅而丢掉独有的有效信息。
63
+ - 只删除过期、被推翻、重复或不属于当前 scope 的条目。
64
+ - 将 summary.md 重写为 1–3 句的目录描述,只用于 topic 选择;它不是权威记忆,也不能用来补偿 content.md 的信息缺失。
63
65
  - 同一批对话也会被处理到其他作用域。
64
66
  这里只处理与当前作用域相关的内容,忽略其他内容。
65
67
 
@@ -70,4 +72,4 @@ Scope: {{target}}
70
72
  "⚠️ 与章程冲突——确认哪一条是当前事实" 并继续。
71
73
 
72
74
  只回复严格 JSON,结构如下:
73
- { "memory_md": "...", "summary_md": "..." }
75
+ { "content_md": "...", "summary_md": "..." }