@yolk_vat-y/dsh-project-memory 0.4.4 → 0.5.0

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.
@@ -0,0 +1,146 @@
1
+ // PR 1b 反思管线(v0.5,默认关)
2
+ // - 触发:select_task 切走旧任务 / archive_task 与 /task archive /(预留空闲与 compaction 前)
3
+ // - 输入不依赖对话原文:task 快照(title + steps + files)相对上次反思的摘要变更(digest 门控)
4
+ // - 冷却:同任务 cooldownMs 内、内容摘要未变 → 跳过(LLM 零调用)
5
+ // - 产出只写 task 级草稿(source: reflect, draft: true),不直接写 project/global;
6
+ // 由 insight-store 的跨任务去重/自动提升语义处理(2 个独立任务命中同一坑 → 升 project)
7
+ // - 任何失败只记日志,绝不抛出、不阻塞调用方
8
+ import { createHash } from 'node:crypto'
9
+ import { ProjectMemoryStore } from './store.js'
10
+ import { memoryRootFor } from './util/fs.js'
11
+ import { cfgInsight, saveInsight, GlobalStore, defaultGlobalFile } from './insight-store.js'
12
+ import { chatText, parseStructuredJson } from './llm.js'
13
+
14
+ export const REFLECT_SYSTEM =
15
+ 'You distill task work into durable lessons and decisions for project memory. ' +
16
+ 'Only record things worth remembering: a real pitfall hit and fixed, a correction received, or a deliberate tradeoff decision. ' +
17
+ 'Normal development does not produce entries. ' +
18
+ 'Return a STRICT JSON object: {"lessons":[{"pattern":"具体错误模式","fix":"修正做法","files":["rel/path.ts"],"symbols":["fnName"],"confidence":0.8}], ' +
19
+ '"decisions":[{"topic":"主题","choice":"选了什么","reason":"为什么","confidence":0.9}]}. ' +
20
+ 'No markdown fences, no commentary, only the JSON object.'
21
+
22
+ export function taskReflectionSnapshot(task) {
23
+ return {
24
+ title: task ? task.title : '',
25
+ steps: ((task && task.steps) || []).map((s) => (typeof s === 'string' ? s : s.content || s.text || '')).filter(Boolean).join('\n'),
26
+ files: ((task && task.files) || []).join('\n'),
27
+ }
28
+ }
29
+
30
+ export function taskDigest(task) {
31
+ const snap = taskReflectionSnapshot(task)
32
+ return createHash('sha256')
33
+ .update(JSON.stringify({ steps: snap.steps, files: snap.files }))
34
+ .digest('hex')
35
+ .slice(0, 16)
36
+ }
37
+
38
+ /** 反思是否到期(enabled / 未反思过 / 摘要已变 / 冷却已过)。 */
39
+ export function isReflectDue(config, task, nowMs = Date.now()) {
40
+ const rc = (config && config.reflection) || {}
41
+ if (rc.enabled === false || rc.enabled == null) return { due: false, reason: 'disabled' }
42
+ if (!task) return { due: false, reason: 'no-task' }
43
+ const last = task.reflection || {}
44
+ if (!last.lastAt) return { due: true, reason: 'never' }
45
+ if (last.digest === taskDigest(task)) return { due: false, reason: 'unchanged' }
46
+ const cooldown = typeof rc.cooldownMs === 'number' ? rc.cooldownMs : 1800000
47
+ if (nowMs - Date.parse(last.lastAt) < cooldown) return { due: false, reason: 'cooldown' }
48
+ return { due: true, reason: 'changed' }
49
+ }
50
+
51
+ function normalizeList(arr, max) {
52
+ if (!Array.isArray(arr)) return []
53
+ return arr.filter((x) => x && typeof x === 'object').slice(0, max)
54
+ }
55
+
56
+ /**
57
+ * 对某任务做一次反思(可归档任务照做——归档正是收割时机)。
58
+ * 全程 try/catch:调用方 fire-and-forget 即可。
59
+ */
60
+ export async function reflectTaskAfter({ config, llm, root, taskId, reason = 'transition' }) {
61
+ const rc = (config && config.reflection) || {}
62
+ const fallback = { ok: false, skipped: 'disabled' }
63
+ if (!rc.enabled) return fallback
64
+ if (!llm) return { ok: false, skipped: 'no-llm' }
65
+ try {
66
+ const store = new ProjectMemoryStore(memoryRootFor(root, config.memoryDir)).load()
67
+ const task = store.getTask(taskId)
68
+ const gate = isReflectDue(config, task)
69
+ if (!gate.due) return { ok: false, skipped: gate.reason }
70
+
71
+ const snap = taskReflectionSnapshot(task)
72
+ const user =
73
+ `Task: ${snap.title || '(untitled)'}\n` +
74
+ (snap.steps ? `Steps:\n${snap.steps.slice(0, 2000)}\n` : '') +
75
+ (snap.files ? `Files touched:\n${snap.files.slice(0, 1000)}\n` : '') +
76
+ `Trigger: ${reason}\n\nReturn the JSON object.`
77
+
78
+ const raw = await chatText(llm, REFLECT_SYSTEM, user, { timeoutMs: 90000 })
79
+ const parsed = parseStructuredJson(raw)
80
+ if (!parsed) return { ok: false, skipped: 'unparsable' }
81
+
82
+ const cfg = cfgInsight(config)
83
+ const globalStore = new GlobalStore(cfg.globalFile || defaultGlobalFile()).load()
84
+ const maxLessons = typeof rc.maxLessonsPerReflect === 'number' ? rc.maxLessonsPerReflect : 3
85
+ const maxDecisions = typeof rc.maxDecisionsPerReflect === 'number' ? rc.maxDecisionsPerReflect : 2
86
+ const written = []
87
+
88
+ for (const l of normalizeList(parsed.lessons, maxLessons)) {
89
+ if (!(l.pattern || l.title)) continue
90
+ const res = store.commit((s) =>
91
+ saveInsight({
92
+ store: s,
93
+ globalStore,
94
+ raw: {
95
+ title: l.title || l.pattern,
96
+ kind: 'lesson',
97
+ pattern: l.pattern,
98
+ fix: l.fix,
99
+ files: l.files,
100
+ symbols: l.symbols,
101
+ confidence: typeof l.confidence === 'number' ? l.confidence : 0.7,
102
+ draft: true,
103
+ },
104
+ scope: 'task',
105
+ taskId,
106
+ cfg,
107
+ source: 'reflect',
108
+ }),
109
+ )
110
+ if (res && res.ok) written.push({ kind: 'lesson', id: res.id, action: res.action })
111
+ }
112
+ for (const d of normalizeList(parsed.decisions, maxDecisions)) {
113
+ if (!(d.topic || d.choice)) continue
114
+ const res = store.commit((s) =>
115
+ saveInsight({
116
+ store: s,
117
+ globalStore,
118
+ raw: { title: d.topic, kind: 'decision', choice: d.choice, reason: d.reason, confidence: typeof d.confidence === 'number' ? d.confidence : 0.7, draft: true },
119
+ scope: 'task',
120
+ taskId,
121
+ cfg,
122
+ source: 'reflect',
123
+ }),
124
+ )
125
+ if (res && res.ok) written.push({ kind: 'decision', id: res.id, action: res.action })
126
+ }
127
+ // 标记反思进度(冷却/摘要以磁盘为准)
128
+ const now = new Date().toISOString()
129
+ store.commit((s) => s.updateTask(taskId, { reflection: { lastAt: now, digest: taskDigest(task) } }))
130
+ globalStore.commit() // task 路径可能链到 global(命中既有知识)
131
+ return { ok: true, reason, written }
132
+ } catch (err) {
133
+ console.error(`[dsh-project-memory] reflection failed for ${taskId}: ${err?.message || err}`)
134
+ return { ok: false, skipped: 'error', error: String(err?.message || err) }
135
+ }
136
+ }
137
+
138
+ /** 工具/命令层 fire-and-forget:默认关时立刻廉价返回,绝不让错误上抛。 */
139
+ export function fireReflect(config, host, root, taskId, reason) {
140
+ const rc = (config && config.reflection) || {}
141
+ if (!rc.enabled) return Promise.resolve({ ok: false, skipped: 'disabled' })
142
+ return reflectTaskAfter({ config, llm: host?.llm, root, taskId, reason }).catch((err) => {
143
+ console.error(`[dsh-project-memory] fireReflect error: ${err?.message || err}`)
144
+ return { ok: false, skipped: 'error' }
145
+ })
146
+ }
@@ -0,0 +1,52 @@
1
+ // 通用文本相似度:双向 token overlap(归一化 0..1)
2
+ // v0.5 去重 / 合并 / 强化共用的**唯一**相似度指标;BM25 只用于排序,禁止当阈值。
3
+ //
4
+ // 归一化定义:normalized = 共同 token 数 / 两文本中较长者的 token 数(= min(ratioA, ratioB))。
5
+ // 它与 v0.4 experience 层 "双向 ratio ≥ t" 判定等价:
6
+ // ratioA ≥ t 且 ratioB ≥ t ⟺ overlap / max(lenA, lenB) ≥ t
7
+ // 因此 "≥ 0.7 合并 / 0.65~0.7 强化" 只需在此单一尺度上比较,跨库/跨长度可比。
8
+ import { tokenize } from './util/search.js'
9
+
10
+ /**
11
+ * 归一化双向 token overlap。
12
+ * @param {string} a
13
+ * @param {string} b
14
+ * @returns {number} 0..1
15
+ */
16
+ export function normalizedTokenOverlap(a, b) {
17
+ if (!a || !b) return 0
18
+ const ta = tokenize(String(a))
19
+ const tb = tokenize(String(b))
20
+ if (!ta.length || !tb.length) return 0
21
+ const sa = new Set(ta)
22
+ const sb = new Set(tb)
23
+ let overlap = 0
24
+ for (const t of sa) if (sb.has(t)) overlap++
25
+ return overlap / Math.max(sa.size, sb.size)
26
+ }
27
+
28
+ /** 一条 insight 参与相似度判定的文本(标题 + kind 专属正文)。 */
29
+ export function insightMatchText(ins) {
30
+ return `${ins.title || ''} ${ins.problem || ''} ${ins.pattern || ''} ${ins.topic || ''} ${ins.choice || ''} ${ins.body || ''}`.trim()
31
+ }
32
+
33
+ /**
34
+ * 在候选列表里找与 text 重叠分最高(且未归档)的一条。
35
+ * @param {string} text
36
+ * @param {Array<object>} items
37
+ * @param {(item: object) => string} [getText]
38
+ * @returns {{ item: object, score: number } | null}
39
+ */
40
+ export function findBestOverlapMatch(text, items, getText = (it) => insightMatchText(it)) {
41
+ let best = null
42
+ let bestScore = 0
43
+ for (const item of items) {
44
+ if (item && item.archived) continue
45
+ const score = normalizedTokenOverlap(text, getText(item))
46
+ if (score > bestScore) {
47
+ bestScore = score
48
+ best = item
49
+ }
50
+ }
51
+ return best ? { item: best, score: bestScore } : null
52
+ }
package/src/store.js CHANGED
@@ -10,6 +10,7 @@ const EXPERIENCE_FILE = 'experience.json'
10
10
  const TASKS_FILE = 'tasks.json'
11
11
  const BINDING_FILE = 'binding.json'
12
12
  const WATCH_FILE = 'watch.json'
13
+ const INSIGHTS_FILE = 'insights.json'
13
14
  const SHARDS_DIR = 'shards'
14
15
 
15
16
  const storeCache = new Map()
@@ -60,12 +61,14 @@ export class ProjectMemoryStore {
60
61
  this.files = {}
61
62
  this.entries = {}
62
63
  this.experience = []
64
+ this.insights = { version: 1, migratedAt: null, items: [] }
63
65
  this.tasks = []
64
66
  this.binding = {}
65
67
  this.watchlist = []
66
68
  this._dirtyShards = new Set()
67
69
  this._removedShards = new Set()
68
70
  this._dirtyExperience = false
71
+ this._dirtyInsights = false
69
72
  this._dirtyTasks = false
70
73
  this._dirtyBinding = false
71
74
  this._dirtyWatch = false
@@ -80,6 +83,7 @@ export class ProjectMemoryStore {
80
83
  if (hot && hot !== this) return hot
81
84
  this._migrateLegacyIfNeeded()
82
85
  this._loadSharded()
86
+ this._loadInsights()
83
87
  storeCache.set(key, this)
84
88
  return this
85
89
  }
@@ -146,6 +150,71 @@ export class ProjectMemoryStore {
146
150
  this._formatWritten = existsSafe(path.join(this.dir, FORMAT_FILE))
147
151
  }
148
152
 
153
+ // ---- v0.5 insights:project 级 insight 文档(任务级在 task.insights[]) ----
154
+ // 迁移语义(与初版方案的差异,见 PLAN §10 风险 / 收尾说明):
155
+ // v0.4 experience.json 仍由 remember/forget/query_memory 服务,不删除;
156
+ // 首次加载把旧笔记**复制导入** insights.json(kind: experience, source: migrate),
157
+ // migratedAt 落盘保证跨进程/崩溃幂等。销毁式收敛放到 recall 统一 PR。
158
+ _loadInsights() {
159
+ const doc = loadJson(path.join(this.dir, INSIGHTS_FILE), null)
160
+ this.insights = doc && typeof doc === 'object' && Array.isArray(doc.items) ? doc : { version: 1, migratedAt: null, items: [] }
161
+ this._migrateExperienceToInsights()
162
+ }
163
+
164
+ _migrateExperienceToInsights() {
165
+ if (this.insights.migratedAt) return
166
+ const legacy = this.experience
167
+ if (!Array.isArray(legacy) || legacy.length === 0) return
168
+ const now = new Date().toISOString()
169
+ const known = new Set(this.insights.items.map((it) => it.id))
170
+ let imported = 0
171
+ for (const item of legacy) {
172
+ if (!item || !item.problem || known.has(item.id)) continue
173
+ known.add(item.id)
174
+ this.insights.items.push({
175
+ id: item.id,
176
+ kind: 'experience',
177
+ scope: 'project',
178
+ title: String(item.problem).slice(0, 120),
179
+ problem: item.problem,
180
+ solution: item.solution,
181
+ files: item.sourceFile ? [item.sourceFile] : [],
182
+ symbols: [],
183
+ sourceTaskIds: [],
184
+ source: 'migrate',
185
+ confidence: 1,
186
+ hitCount: 0,
187
+ createdAt: item.createdAt || now,
188
+ updatedAt: item.updatedAt || now,
189
+ })
190
+ imported++
191
+ }
192
+ this.insights.migratedAt = now
193
+ try {
194
+ mkdirSync(this.dir, { recursive: true })
195
+ writeJsonAtomic(path.join(this.dir, INSIGHTS_FILE), this.insights)
196
+ } catch (err) {
197
+ console.error(`[dsh-project-memory] insights migration write failed: ${err.message}`)
198
+ }
199
+ }
200
+
201
+ insightsDoc() {
202
+ return this.insights
203
+ }
204
+
205
+ insightItems() {
206
+ return this.insights.items
207
+ }
208
+
209
+ replaceInsightItems(items) {
210
+ this.insights.items = items
211
+ this._dirtyInsights = true
212
+ }
213
+
214
+ markTasksDirty() {
215
+ this._dirtyTasks = true
216
+ }
217
+
149
218
  cleanStaleTmp() {
150
219
  const scanDirs = [this.dir, path.join(this.dir, SHARDS_DIR)]
151
220
  const now = Date.now()
@@ -195,6 +264,10 @@ export class ProjectMemoryStore {
195
264
  writeJsonAtomic(path.join(this.dir, EXPERIENCE_FILE), this.experience)
196
265
  this._dirtyExperience = false
197
266
  }
267
+ if (this._dirtyInsights) {
268
+ writeJsonAtomic(path.join(this.dir, INSIGHTS_FILE), this.insights)
269
+ this._dirtyInsights = false
270
+ }
198
271
  if (this._dirtyTasks) {
199
272
  writeJsonAtomic(path.join(this.dir, TASKS_FILE), this.tasks)
200
273
  this._dirtyTasks = false
@@ -0,0 +1,101 @@
1
+ import { defineTool } from '@deepseek-ai/dsh-tools'
2
+ import { memoryRootFor, resolveIndexRoot } from '../util/fs.js'
3
+ import { ProjectMemoryStore } from '../store.js'
4
+ import { truncate } from '../util/text.js'
5
+ import { cfgInsight, saveInsight, defaultGlobalFile, GlobalStore } from '../insight-store.js'
6
+ import { ensureGlobalInit } from '../global-seed.js'
7
+ import { INSIGHT_KINDS, INSIGHT_SCOPES } from '../insight-store.js'
8
+
9
+ function sessionIdOf(exec) {
10
+ return exec?.agent?.session?.id || exec?.ctx?.session?.id
11
+ }
12
+
13
+ export function lessonTool(config) {
14
+ const kindDesc = `insight 语义:lesson(曾踩坑/纠偏, pattern→fix) | decision(权衡选型, choice→reason) | procedure(多步指南, steps+可选 trigger) | experience(problem→solution,v0.4 兼容)。默认 lesson。`
15
+ const scopeDesc =
16
+ '作用域:task(任务私有,随任务归档,不进共享注入) | project(项目资产) | global(个人能力库,跨项目,建议配 trigger 关键词以便将来技能命中)。' +
17
+ '解析顺序:显式 scope > task_id > 当前绑定任务 > project。'
18
+ return defineTool({
19
+ name: 'save_lesson',
20
+ description:
21
+ 'Save a lesson/decision/procedure/experience into the tiered memory (single insight entity). ' +
22
+ 'Call it when the task hit a real pitfall, got corrected, or made a deliberate choice worth remembering — not for routine work. ' +
23
+ 'Similar entries auto-merge (bidirectional token overlap >= 0.7) or reinforce (0.65~0.7, accumulating task hits); ' +
24
+ 'when the same insight is hit by 2+ tasks it auto-promotes task -> project, 3+ tasks project -> global. ' + kindDesc + ' ' + scopeDesc,
25
+ parameters: {
26
+ title: { type: 'string', description: 'One-line title (lesson: the failure pattern; decision: the topic; procedure: the name).' },
27
+ kind: { type: 'string', enum: INSIGHT_KINDS, description: kindDesc },
28
+ scope: { type: 'string', enum: INSIGHT_SCOPES, description: scopeDesc },
29
+ pattern: { type: 'string', description: 'kind=lesson: 具体错误模式(同 title 时二选一)' },
30
+ fix: { type: 'string', description: 'kind=lesson: 修正做法' },
31
+ choice: { type: 'string', description: 'kind=decision: 选了什么' },
32
+ reason: { type: 'string', description: 'kind=decision: 为什么(权衡点)' },
33
+ steps: { type: 'array', items: { type: 'string' }, description: 'kind=procedure: 有序步骤(未来静默注入的正文)' },
34
+ trigger: {
35
+ type: 'object',
36
+ additionalProperties: false,
37
+ properties: {
38
+ keywords: { type: 'array', items: { type: 'string' } },
39
+ symbols: { type: 'array', items: { type: 'string' } },
40
+ scope: { type: 'array', items: { type: 'string' } },
41
+ },
42
+ description: 'kind=procedure: "作为 Skill" 的触发关键词(keywords/symbols + scope tags)',
43
+ },
44
+ task_id: { type: 'string', description: '目标任务 id(scope 缺省时优先于绑定任务)' },
45
+ files: { type: 'array', items: { type: 'string' }, description: '关联文件(项目相对路径)' },
46
+ symbols: { type: 'array', items: { type: 'string' }, description: '关联符号' },
47
+ confidence: { type: 'number', description: '置信 0..1(默认 0.8)' },
48
+ root: { type: 'string', description: 'Project root. Defaults to current working directory.' },
49
+ },
50
+ output: { schema: { type: 'string' }, render: (_a, v) => [{ type: 'text', text: v }] },
51
+ async execute(args, exec) {
52
+ const root = resolveIndexRoot(exec, args.root)
53
+ const memoryDir = memoryRootFor(root, config.memoryDir)
54
+ const store = new ProjectMemoryStore(memoryDir).load()
55
+ const cfg = cfgInsight(config)
56
+ const sid = sessionIdOf(exec)
57
+ const boundTaskId = sid ? store.getBoundTaskId(sid) : undefined
58
+ const taskId = args.task_id || boundTaskId
59
+
60
+ const raw = {
61
+ title: args.title,
62
+ kind: args.kind,
63
+ pattern: args.pattern,
64
+ fix: args.fix,
65
+ choice: args.choice,
66
+ reason: args.reason,
67
+ steps: args.steps,
68
+ trigger: args.trigger,
69
+ files: args.files,
70
+ symbols: args.symbols,
71
+ confidence: args.confidence,
72
+ }
73
+ if (!raw.title && !raw.pattern && !raw.choice) {
74
+ return truncate(JSON.stringify({ success: false, error: '需要 title / pattern / choice 之一' }), config.maxOutputChars || 8000)
75
+ }
76
+ if (!INSIGHT_KINDS.includes(raw.kind)) raw.kind = 'lesson'
77
+
78
+ let scope = args.scope
79
+ if (!scope) scope = taskId ? 'task' : 'project'
80
+ if (scope === 'task' && !taskId) {
81
+ return truncate(JSON.stringify({ success: false, error: 'task 级写入需要 task_id(或先 select_task 绑定会话)' }), config.maxOutputChars || 8000)
82
+ }
83
+
84
+ ensureGlobalInit(config) // 幂等确保 global 文件结构就绪
85
+ const globalStore = new GlobalStore(cfg.globalFile || defaultGlobalFile()).load()
86
+
87
+ let result
88
+ store.commit((s) => {
89
+ result = saveInsight({ store: s, globalStore, raw, scope, taskId, cfg, source: 'agent' })
90
+ return result
91
+ })
92
+ // global 变更(global scope 直写或 project 自动提升)单独落盘
93
+ globalStore.commit()
94
+ if (!result) result = { ok: false, error: '写入未完成' }
95
+ const body = result.ok
96
+ ? `Saved insight ${result.id} [scope=${result.scope || scope}] action=${result.action}${result.hint ? ` — ${result.hint}` : ''}`
97
+ : `Failed: ${result.error}`
98
+ return truncate(body, config.maxOutputChars || 8000)
99
+ },
100
+ })
101
+ }
@@ -3,6 +3,7 @@ import { memoryRootFor, resolveIndexRoot } from '../util/fs.js'
3
3
  import { ProjectMemoryStore } from '../store.js'
4
4
  import { truncate } from '../util/text.js'
5
5
  import { genTaskId, hash8, adoptStepsToSession, shouldAdoptToHost } from '../setup/taskbridge.js'
6
+ import { fireReflect } from '../reflection-pipeline.js'
6
7
  import { createHash } from 'node:crypto'
7
8
 
8
9
  function sessionIdOf(exec) {
@@ -51,7 +52,7 @@ export function listTasksTool(config) {
51
52
  })
52
53
  }
53
54
 
54
- export function selectTaskTool(config) {
55
+ export function selectTaskTool(config, host) {
55
56
  return defineTool({
56
57
  name: 'select_task',
57
58
  description:
@@ -68,6 +69,7 @@ export function selectTaskTool(config) {
68
69
  const sid = sessionIdOf(exec)
69
70
  const now = new Date().toISOString()
70
71
  const error = (msg) => truncate(JSON.stringify({ success: false, error: msg }), config.maxOutputChars)
72
+ const prevBound = sid ? store.getBoundTaskId(sid) : undefined
71
73
 
72
74
  if (!args.taskId && !args.title) {
73
75
  return error('需要 taskId 或 title(先 list_tasks;未绑定会话出现 todo 时会自动新建任务)')
@@ -133,11 +135,17 @@ export function selectTaskTool(config) {
133
135
  adoptStepsToSession(exec?.agent?.session || exec?.ctx?.session, task)
134
136
  }
135
137
 
138
+ // 反思钩子:切走旧任务时异步收割(默认关,零成本;失败只记日志)
139
+ if (prevBound && prevBound !== task.id) {
140
+ fireReflect(config, host, root, prevBound, 'switch-away')
141
+ }
142
+
136
143
  const card = {
137
144
  taskId: task.id,
138
145
  title: task.title,
139
146
  steps: task.steps,
140
147
  files: task.files,
148
+ insights: task.insights || [], // v0.5:任务级 lessons/decisions/procedures
141
149
  hint: hint + ';请据此用 todo_write 重建本会话清单',
142
150
  }
143
151
  return truncate(JSON.stringify(card), config.maxOutputChars)
@@ -164,7 +172,7 @@ export function showTaskPanelTool(config) {
164
172
  })
165
173
  }
166
174
 
167
- export function archiveTaskTool(config) {
175
+ export function archiveTaskTool(config, host) {
168
176
  return defineTool({
169
177
  name: 'archive_task',
170
178
  description: 'Archive a task: hide from default views, exclude from capacity, stop syncing. Restore by selecting it again.',
@@ -182,6 +190,8 @@ export function archiveTaskTool(config) {
182
190
  task.archived = true
183
191
  task.updatedAt = new Date().toISOString()
184
192
  store.save()
193
+ // 反思钩子:归档即收割最终教训(默认关)
194
+ fireReflect(config, host, root, args.taskId, 'archive')
185
195
  return truncate(JSON.stringify({ success: true, archived: true, hint: '已归档,select_task 可恢复' }), config.maxOutputChars)
186
196
  },
187
197
  })
package/src/types.js ADDED
@@ -0,0 +1,56 @@
1
+ // JSDoc 类型定义(服务端一律 .js;本文件仅供编辑器/阅读共享心智,无运行时导出)。
2
+ // v0.5 单一 insight 实体:kind 区分语义,scope 分层,提升 = scope 字段变更。
3
+
4
+ /**
5
+ * @typedef {'lesson' | 'decision' | 'procedure' | 'experience'} InsightKind
6
+ * lesson: 曾踩坑/纠偏(pattern→fix);decision: 曾权衡的选型(topic→choice+reason);
7
+ * procedure: 多步操作指南(steps);experience: v0.4 旧笔记迁移而来(problem→solution)。
8
+ *
9
+ * @typedef {'task' | 'project' | 'global'} InsightScope
10
+ * task: tasks.json 内嵌于任务实体 task.insights[](私有草稿);
11
+ * project: .dsh-project-memory/insights.json(团队/项目资产);
12
+ * global: ~/.config/dsh-project-memory/global.json(个人能力库,跨项目)。
13
+ *
14
+ * @typedef {object} Insight
15
+ * @property {string} id 全局唯一(新条目 "ins_…";迁移条目沿用旧 uuid)
16
+ * @property {InsightKind} kind
17
+ * @property {InsightScope} scope
18
+ * @property {boolean} [draft] true = 反思自动产出,未审核、不参与注入
19
+ * @property {string} title 一句话标题
20
+ * @property {string} [body] 正文
21
+ * @property {string[]} [steps] procedure 有序步骤
22
+ * @property {string} [choice] decision:选项
23
+ * @property {string} [reason] decision:理由
24
+ * @property {string} [pattern] lesson:错误模式
25
+ * @property {string} [fix] lesson:修正做法
26
+ * @property {string} [problem] experience:问题(旧字段保留)
27
+ * @property {string} [solution] experience:解法(旧字段保留)
28
+ * @property {string[]} [files] 关联代码位置
29
+ * @property {string[]} [symbols] 关联符号
30
+ * @property {string[]} [sourceTaskIds] 曾在哪些任务中出现/被命中(提升判定唯一依据)
31
+ * @property {'reflect' | 'agent' | 'user' | 'migrate'} [source] 来源
32
+ * @property {{ scope: string, id: string, at: string }} [movedFrom] 提升/降级审计
33
+ * @property {{ keywords: string[], symbols?: string[], scope?: string[] }} [trigger]
34
+ * procedure 的 "生成 Skill" 触发规则(无独立 skill 实体)
35
+ * @property {number} [confidence] 0..1
36
+ * @property {number} [hitCount] 被注入/命中的次数(强化信号,无 LLM)
37
+ * @property {string} [lastHitAt] 最近一次命中时间 ISO(衰减/归档依据)
38
+ * @property {boolean} [archived] 归档 = 软删:不进召回/注入,可恢复
39
+ * @property {string[]} [tags]
40
+ * @property {string} createdAt
41
+ * @property {string} updatedAt
42
+ */
43
+
44
+ /**
45
+ * @typedef {object} InsightConfig
46
+ * @property {number} [dedupOverlap] 双向 token overlap 阈值(≥ 合并/取代)
47
+ * @property {number} [reinforceBand] 近重复强化带下界(0.65~dedupOverlap → 强化)
48
+ * @property {number} [maxProject] project insights 容量(归档区另计)
49
+ * @property {number} [maxGlobalProcedures] global insights 容量
50
+ * @property {number} [promoteConfidence] 提升 project 所需最低 confidence
51
+ * @property {number} [globalPromoteTasks] sourceTaskIds ≥ N → 提升 global
52
+ * @property {number} [decayDays] 久置归档启发:hitCount==0 超该天数 → 归档(软删)
53
+ * @property {string} [globalFile] global.json 绝对路径(默认 ~/.config/dsh-project-memory/global.json)
54
+ */
55
+
56
+ export {}