@aiwayds/dsh-topics-memory 0.13.0 → 0.14.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.
- package/README.md +4 -0
- package/README.zh.md +4 -0
- package/lib/commands.d.ts +8 -1
- package/lib/commands.js +75 -5
- package/lib/config.d.ts +32 -1
- package/lib/config.js +25 -1
- package/lib/consolidate.d.ts +138 -0
- package/lib/consolidate.js +571 -0
- package/lib/ilog.d.ts +18 -0
- package/lib/ilog.js +47 -0
- package/lib/index.js +61 -5
- package/lib/retrieval.d.ts +17 -1
- package/lib/retrieval.js +16 -0
- package/lib/service.d.ts +10 -1
- package/lib/service.js +78 -1
- package/lib/store.d.ts +11 -0
- package/lib/store.js +35 -1
- package/package.json +2 -2
- package/skills/{dsh-topics-memory → dsh-topics-memory-config}/SKILL.md +13 -4
- package/src/commands.ts +84 -5
- package/src/config.ts +27 -1
- package/src/consolidate.ts +639 -0
- package/src/ilog.ts +53 -0
- package/src/index.ts +59 -5
- package/src/retrieval.ts +28 -1
- package/src/service.ts +86 -1
- package/src/store.ts +38 -2
package/src/commands.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { join } from 'node:path'
|
|
|
12
12
|
import { spawn } from 'node:child_process'
|
|
13
13
|
import type { TopicsService } from './service.ts'
|
|
14
14
|
import type { DistillResult } from './distill.ts'
|
|
15
|
+
import type { ConsolidateResult } from './consolidate.ts'
|
|
15
16
|
import { serializeTopicDoc, slugify, firstParagraph } from './okf.ts'
|
|
16
17
|
import { buildGraph, renderGraphHtml } from './viz.ts'
|
|
17
18
|
import { CONFIG_KEYS, displayKey, type ConfigKey, type TopicsConfigValue, parseConfigValue } from './config.ts'
|
|
@@ -37,11 +38,19 @@ import {
|
|
|
37
38
|
*/
|
|
38
39
|
export type ManualDistill = (invocation: CommandInvocation) => Promise<DistillResult>
|
|
39
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Manual `/topics consolidate` trigger, wired by the host (index.ts owns the
|
|
43
|
+
* lane instance). Bypasses the cadence gate; the lane's own in-flight guard
|
|
44
|
+
* still applies.
|
|
45
|
+
*/
|
|
46
|
+
export type ManualConsolidate = (invocation: CommandInvocation) => Promise<ConsolidateResult>
|
|
47
|
+
|
|
40
48
|
export const HELP = [
|
|
41
49
|
'dsh-topics-memory — OKF topic 记忆(本地 bundle,git 可追溯,可选 GitHub 同步)',
|
|
42
50
|
' /topics onboard 交互式配置向导(ask-user 面板逐项问答;无 UI 环境逐条输入)',
|
|
43
51
|
' /topics status bundle 健康:topic 数、观察积压、冲突、同步状态',
|
|
44
52
|
' /topics distill 手动触发一次蒸馏(观察池 → Topic,输出 marked/created/updated/gc 摘要)',
|
|
53
|
+
' /topics consolidate 手动触发一次整理(合并重复/晋升 stable/废弃过时/刷新元数据,无视 cadence 立即跑)',
|
|
45
54
|
' /topics stats 注入统计:hit rate、top-N、near-miss 分布与调参建议',
|
|
46
55
|
' /topics list 列出全部 Topic',
|
|
47
56
|
' /topics show <slug> 查看一个 Topic 全文(含反向引用)',
|
|
@@ -55,17 +64,17 @@ export const HELP = [
|
|
|
55
64
|
'凭据:$GITHUB_TOKEN 或已登录的 gh CLI;登录不在本插件职责内。',
|
|
56
65
|
].join('\n')
|
|
57
66
|
|
|
58
|
-
export function buildTopicsCommand(service: TopicsService, mutate: MutateFn, resolveAsk: AskServiceResolver = () => undefined, resolveLlm: LlmDirectoryResolver = () => [], distillNow?: ManualDistill): CommandDefinition {
|
|
67
|
+
export function buildTopicsCommand(service: TopicsService, mutate: MutateFn, resolveAsk: AskServiceResolver = () => undefined, resolveLlm: LlmDirectoryResolver = () => [], distillNow?: ManualDistill, consolidateNow?: ManualConsolidate): CommandDefinition {
|
|
59
68
|
const onboard = createOnboardHandler(service, mutate, undefined, resolveAsk, resolveLlm)
|
|
60
69
|
return {
|
|
61
70
|
name: 'topics',
|
|
62
|
-
description: 'OKF topic 记忆:onboard | distill | status | stats | list | show | history | graph | sync | config | set',
|
|
63
|
-
input: { hint: '[onboard | distill | status | stats | list | show <slug> | history <slug> | graph | sync [pull|push] | config | set <key> <value>]' },
|
|
64
|
-
handler: (invocation) => handle(invocation, service, mutate, onboard, resolveAsk, resolveLlm, distillNow),
|
|
71
|
+
description: 'OKF topic 记忆:onboard | distill | consolidate | status | stats | list | show | history | graph | sync | config | set',
|
|
72
|
+
input: { hint: '[onboard | distill | consolidate | status | stats | list | show <slug> | history <slug> | graph | sync [pull|push] | config | set <key> <value>]' },
|
|
73
|
+
handler: (invocation) => handle(invocation, service, mutate, onboard, resolveAsk, resolveLlm, distillNow, consolidateNow),
|
|
65
74
|
}
|
|
66
75
|
}
|
|
67
76
|
|
|
68
|
-
async function handle(invocation: CommandInvocation, service: TopicsService, mutate: MutateFn, onboard: (args: string[], invocation: CommandInvocation) => Promise<CommandResult>, resolveAsk: AskServiceResolver, resolveLlm: LlmDirectoryResolver, distillNow?: ManualDistill): Promise<CommandResult> {
|
|
77
|
+
async function handle(invocation: CommandInvocation, service: TopicsService, mutate: MutateFn, onboard: (args: string[], invocation: CommandInvocation) => Promise<CommandResult>, resolveAsk: AskServiceResolver, resolveLlm: LlmDirectoryResolver, distillNow?: ManualDistill, consolidateNow?: ManualConsolidate): Promise<CommandResult> {
|
|
69
78
|
const raw = invocation.rawInput.trim()
|
|
70
79
|
const [action = '', ...rest] = raw.split(/\s+/)
|
|
71
80
|
try {
|
|
@@ -78,6 +87,8 @@ async function handle(invocation: CommandInvocation, service: TopicsService, mut
|
|
|
78
87
|
return ok(await renderStatus(service))
|
|
79
88
|
case 'distill':
|
|
80
89
|
return await doDistill(service, invocation, distillNow)
|
|
90
|
+
case 'consolidate':
|
|
91
|
+
return await doConsolidate(service, invocation, consolidateNow)
|
|
81
92
|
case 'stats':
|
|
82
93
|
return ok(await renderStats(service))
|
|
83
94
|
case 'list':
|
|
@@ -125,6 +136,7 @@ async function renderStatus(service: TopicsService): Promise<string> {
|
|
|
125
136
|
['注入去重', cfg.injectDedup ? '开(同会话已注入的 Topic 不重注)' : '关'],
|
|
126
137
|
['慢道', cfg.qualityLane === 'off' ? '关' : cfg.qualityLane === 'always' ? `每轮(${cfg.distillProvider}/${cfg.distillModel})` : `采样 1/3(${cfg.distillProvider}/${cfg.distillModel})`],
|
|
127
138
|
['蒸馏', cfg.distillProvider !== '' && cfg.distillModel !== '' ? `${cfg.distillProvider}/${cfg.distillModel},每 ${cfg.distillEveryTurns} 轮` : '未配置模型(/topics set distill-provider / distill-model)'],
|
|
139
|
+
['整理', cadenceLabel(cfg.consolidateCadence) + (cfg.consolidateCadence === 'off' ? '' : `(复用蒸馏模型,/topics consolidate 立即跑)`)],
|
|
128
140
|
]
|
|
129
141
|
// Last lane outcome (distill-state summary) — what "checkable via
|
|
130
142
|
// /topics status" promises; absent until the first run of this bundle.
|
|
@@ -137,6 +149,20 @@ async function renderStatus(service: TopicsService): Promise<string> {
|
|
|
137
149
|
const gc = typeof lastRun.gcDropped === 'number' && lastRun.gcDropped > 0 ? `,GC 回收 ${lastRun.gcDropped}` : ''
|
|
138
150
|
rows.push(['最近蒸馏', `${outcome}${gc} @ ${cell(String(lastRun.at ?? '').slice(0, 19).replace('T', ' '))}`])
|
|
139
151
|
}
|
|
152
|
+
const lastConsolidate = await service.store.readConsolidateState()
|
|
153
|
+
if (lastConsolidate !== undefined) {
|
|
154
|
+
const count = (v: unknown): number => (Array.isArray(v) ? v.length : 0)
|
|
155
|
+
const counts = [
|
|
156
|
+
`合并 ${count(lastConsolidate.merged)}`,
|
|
157
|
+
`晋升 ${count(lastConsolidate.promoted)}`,
|
|
158
|
+
`废弃 ${count(lastConsolidate.deprecated)}`,
|
|
159
|
+
`刷新 ${count(lastConsolidate.refreshed)}`,
|
|
160
|
+
].join('/')
|
|
161
|
+
rows.push([
|
|
162
|
+
'最近整理',
|
|
163
|
+
`${counts} @ ${cell(String(lastConsolidate.at ?? '').slice(0, 19).replace('T', ' '))}${lastConsolidate.ok === false && lastConsolidate.detail ? `(${String(lastConsolidate.detail).slice(0, 80)})` : ''}`,
|
|
164
|
+
])
|
|
165
|
+
}
|
|
140
166
|
if (service.sync !== undefined) {
|
|
141
167
|
rows.push(['上次推送', cell(String(service.sync.lastPushAt ?? '从未'))])
|
|
142
168
|
if (service.sync.lastError !== '') rows.push(['同步错误', cell(service.sync.lastError.split('\n').at(-1) ?? '')])
|
|
@@ -156,6 +182,59 @@ async function renderStatus(service: TopicsService): Promise<string> {
|
|
|
156
182
|
* summary mirrors the distill state fields (ok / marked / created / updated /
|
|
157
183
|
* gc dropped / reason) so what the user sees is what the state file records.
|
|
158
184
|
*/
|
|
185
|
+
/** Human-readable cadence for status/consolidate output. */
|
|
186
|
+
function cadenceLabel(value: string): string {
|
|
187
|
+
switch (value) {
|
|
188
|
+
case 'daily':
|
|
189
|
+
return '每天'
|
|
190
|
+
case '3d':
|
|
191
|
+
return '每 3 天'
|
|
192
|
+
case '7d':
|
|
193
|
+
return '每 7 天'
|
|
194
|
+
default:
|
|
195
|
+
return '关'
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* `/topics consolidate` — one manual run of the consolidation lane (LLM
|
|
201
|
+
* gardener over the existing pool). Bypasses the cadence gate; the lane's
|
|
202
|
+
* own in-flight guard dedups a concurrent automatic run. Output mirrors the
|
|
203
|
+
* lane's actions with per-op reasons so the user can eyeball (and git-revert)
|
|
204
|
+
* what the model decided.
|
|
205
|
+
*/
|
|
206
|
+
async function doConsolidate(service: TopicsService, invocation: CommandInvocation, consolidateNow: ManualConsolidate | undefined): Promise<CommandResult> {
|
|
207
|
+
if (consolidateNow === undefined) return fail('整理 lane 未接线(宿主未提供手动整理触发器)')
|
|
208
|
+
const cfg = service.cfg
|
|
209
|
+
if (cfg.distillProvider === '' || cfg.distillModel === '') {
|
|
210
|
+
return fail('整理复用蒸馏模型路由:请先 /topics set distill-provider 与 distill-model。')
|
|
211
|
+
}
|
|
212
|
+
const result = await consolidateNow(invocation)
|
|
213
|
+
if (!result.ok) {
|
|
214
|
+
const reasonNote =
|
|
215
|
+
result.reason === 'no-clusters'
|
|
216
|
+
? '没有找到可能相近的 topic 候选簇(词面上没有疑似重复/相关对)'
|
|
217
|
+
: result.reason === 'no-model'
|
|
218
|
+
? '蒸馏模型未配置(/topics set distill-provider / distill-model)'
|
|
219
|
+
: result.reason === 'in-flight'
|
|
220
|
+
? '已有整理在跑,稍后再试'
|
|
221
|
+
: (result.reason ?? 'unknown')
|
|
222
|
+
return fail(`整理未执行:${reasonNote}${result.detail !== undefined ? `\n ${result.detail}` : ''}`)
|
|
223
|
+
}
|
|
224
|
+
const lines: string[] = []
|
|
225
|
+
const actions = result.actions ?? []
|
|
226
|
+
const KIND_LABEL: Record<string, string> = { merge: '合并', promote: '晋升', deprecate: '废弃', refresh: '刷新' }
|
|
227
|
+
for (const a of actions) {
|
|
228
|
+
if (a.kind === 'merge' && a.merged !== undefined) {
|
|
229
|
+
lines.push(`- 合并 ${a.merged.map((m) => cell(m)).join('、')} → ${cell(a.slug)}${a.reason !== undefined ? `:${a.reason}` : ''}`)
|
|
230
|
+
} else {
|
|
231
|
+
lines.push(`- ${KIND_LABEL[a.kind] ?? a.kind} ${cell(a.slug)}${a.reason !== undefined ? `:${a.reason}` : ''}`)
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const header = `✅ 整理完成:合并 ${result.merged.length} 组;晋升 ${result.promoted.length};废弃 ${result.deprecated.length};刷新 ${result.refreshed.length}${result.droppedOps ? `(丢弃 ${result.droppedOps} 个无效 op)` : ''}${result.detail !== undefined ? `\n ${result.detail}` : ''}`
|
|
235
|
+
return ok(lines.length === 0 ? header : `${header}\n${lines.join('\n')}\n全部变更已逐条 git commit,可 /topics history <slug> 追溯、git revert 回滚。`)
|
|
236
|
+
}
|
|
237
|
+
|
|
159
238
|
async function doDistill(service: TopicsService, invocation: CommandInvocation, distillNow: ManualDistill | undefined): Promise<CommandResult> {
|
|
160
239
|
if (distillNow === undefined) return fail('蒸馏 lane 未接线(宿主未提供手动蒸馏触发器)')
|
|
161
240
|
const pending = await service.store.undistilledObservations(1)
|
package/src/config.ts
CHANGED
|
@@ -58,6 +58,20 @@ export const TopicsConfig = z.object({
|
|
|
58
58
|
* Default 8: with the batch loop, one run then drains ~30-40 observations
|
|
59
59
|
* instead of ~10, which is what makes a real backlog actually shrink. */
|
|
60
60
|
distillMaxModelCalls: z.number().default(8),
|
|
61
|
+
/** Consolidation lane (整理) cadence: how often a session start may run the
|
|
62
|
+
* LLM gardener over the EXISTING pool (merge near-duplicates, promote
|
|
63
|
+
* settled drafts, deprecate superseded, refresh metadata). Reuses the
|
|
64
|
+
* distill model route; off disables the lane entirely. */
|
|
65
|
+
consolidateCadence: z.string().default('daily'),
|
|
66
|
+
/** Deprecated topics older than this many days are dropped at session start
|
|
67
|
+
* (local TTL sweep, no model; each drop is its own git commit, so history
|
|
68
|
+
* stays recoverable on the remote). 0 disables the sweep. */
|
|
69
|
+
deprecatedTtlDays: z.number().default(15),
|
|
70
|
+
/** UsageBoost (ADR 0015): behavioral bonus for topics injected/opened in
|
|
71
|
+
* the last 30 days, folded into the gate score — capped at 0.2 and never
|
|
72
|
+
* granted to zero-lexical candidates; the structural gate still applies.
|
|
73
|
+
* 0 = off. */
|
|
74
|
+
usageBoost: z.number().default(0.15),
|
|
61
75
|
/** Debounced push delay in GitHub mode. */
|
|
62
76
|
pushDebounceSeconds: z.number().default(45),
|
|
63
77
|
})
|
|
@@ -85,6 +99,9 @@ export type TopicsConfigValue = {
|
|
|
85
99
|
distillModel: string
|
|
86
100
|
distillBatchSize: number
|
|
87
101
|
distillMaxModelCalls: number
|
|
102
|
+
consolidateCadence: string
|
|
103
|
+
deprecatedTtlDays: number
|
|
104
|
+
usageBoost: number
|
|
88
105
|
pushDebounceSeconds: number
|
|
89
106
|
}
|
|
90
107
|
|
|
@@ -111,6 +128,9 @@ export const CONFIG_KEYS = [
|
|
|
111
128
|
'distillModel',
|
|
112
129
|
'distillBatchSize',
|
|
113
130
|
'distillMaxModelCalls',
|
|
131
|
+
'consolidateCadence',
|
|
132
|
+
'deprecatedTtlDays',
|
|
133
|
+
'usageBoost',
|
|
114
134
|
'pushDebounceSeconds',
|
|
115
135
|
] as const
|
|
116
136
|
|
|
@@ -149,6 +169,7 @@ export function parseConfigValue(key: ConfigKey, raw: string): boolean | number
|
|
|
149
169
|
case 'distillEveryTurns':
|
|
150
170
|
case 'distillBatchSize':
|
|
151
171
|
case 'distillMaxModelCalls':
|
|
172
|
+
case 'deprecatedTtlDays':
|
|
152
173
|
case 'pushDebounceSeconds': {
|
|
153
174
|
const n = Number(raw)
|
|
154
175
|
if (!Number.isFinite(n) || n < 0 || !Number.isInteger(n)) return { error: `${key} 需要非负整数` }
|
|
@@ -158,12 +179,17 @@ export function parseConfigValue(key: ConfigKey, raw: string): boolean | number
|
|
|
158
179
|
if (raw === 'pointer' || raw === 'digest') return raw
|
|
159
180
|
return { error: 'inject-mode 取值 pointer|digest' }
|
|
160
181
|
}
|
|
182
|
+
case 'consolidateCadence': {
|
|
183
|
+
if (raw === 'off' || raw === 'daily' || raw === '3d' || raw === '7d') return raw
|
|
184
|
+
return { error: 'consolidate-cadence 取值 off|daily|3d|7d' }
|
|
185
|
+
}
|
|
161
186
|
case 'qualityLane': {
|
|
162
187
|
if (raw === 'off' || raw === 'sampled' || raw === 'always') return raw
|
|
163
188
|
return { error: 'quality-lane 取值 off|sampled|always' }
|
|
164
189
|
}
|
|
165
190
|
case 'matchThreshold':
|
|
166
|
-
case 'tagBoost':
|
|
191
|
+
case 'tagBoost':
|
|
192
|
+
case 'usageBoost': {
|
|
167
193
|
const n = Number(raw)
|
|
168
194
|
if (!Number.isFinite(n) || n < 0) return { error: `${key} 需要非负数` }
|
|
169
195
|
return n
|