@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/README.md
CHANGED
|
@@ -58,6 +58,7 @@ This plugin is not trying to be the model's notebook. It is trying to be the mod
|
|
|
58
58
|
| `/topics onboard` | Interactive setup wizard on dsh-native ask-user panels (mode / repo / distill model / injection tier / auto-observe); typed fallback where no ask-user UI exists |
|
|
59
59
|
| `/topics status` | Bundle health: topic count, observation backlog, conflicts, last distill outcome, sync status |
|
|
60
60
|
| `/topics distill` | Manually trigger one distill run over the current observation pool (same lane, same in-flight guard; summary mirrors the distill-state fields) |
|
|
61
|
+
| `/topics consolidate` | Manually trigger one consolidation run: the LLM gardener merges duplicates, promotes settled drafts, deprecates superseded entries, refreshes metadata — every change is its own git commit, revert to roll back |
|
|
61
62
|
| `/topics stats` | Injection stats: hit rate, top-N, near-miss distribution, tuning advice |
|
|
62
63
|
| `/topics list` / `show` / `history` | Browse topics, backlinks, and change history |
|
|
63
64
|
| `/topics graph` | Generate a relationship-graph web page (force-directed, draggable/zoomable, hover for conclusions) and open it in the browser |
|
|
@@ -121,6 +122,9 @@ First-time setup belongs to `/topics onboard`; day-to-day tuning is `/topics set
|
|
|
121
122
|
| `distillOnSessionEnd` | `true` | Distill once when a session ends |
|
|
122
123
|
| `distillBatchSize` | `40` | Observations per distill model call. On an output-limit (`max-tokens`) failure the batch halves automatically (floor 5) and retries — a failing batch can no longer livelock the backlog; the shrink persists until reload or a config change. Note: `/topics set distillBatchSize` back to the same value does not reset the shrink — set a different value or reload the plugin |
|
|
123
124
|
| `distillMaxModelCalls` | `8` | Max model calls per distill run, including the one corrective retry for ops echoing no valid `observed_ids` (the run stalls when the budget can't fit it). Batches already distilled keep their marks when the budget stops the run (partial progress), recorded as `partial: …` in the distill state |
|
|
125
|
+
| `consolidateCadence` | `daily` | Consolidation-lane cadence: `daily`/`3d`/`7d`/`off`. At session start the plugin checks the last consolidation time (`meta/consolidate-state.json`) and, when due, runs the LLM gardener in the background (reusing the distill model route): local lexical clustering only feeds near-look-alike candidate clusters; four op kinds merge/promote/deprecate/refresh, out-of-scope ops are dropped; a failed call never advances the stamp, so the next start retries |
|
|
126
|
+
| `deprecatedTtlDays` | `15` | Deprecated topics older than N days are dropped at session start (local rule, no model; each drop is its own git commit — history stays recoverable); `0` disables the sweep |
|
|
127
|
+
| `usageBoost` | `0.15` | Usage boost (ADR 0015): topics injected/opened in the last 30 days score higher at retrieval (gate-scoped, capped at 0.2, never granted to zero-lexical candidates, structural gate not waived); `0` disables |
|
|
124
128
|
| `pushDebounceSeconds` | `45` | GitHub-mode debounced push interval |
|
|
125
129
|
|
|
126
130
|
## Acknowledgements
|
package/README.zh.md
CHANGED
|
@@ -52,6 +52,7 @@ https://github.com/user-attachments/assets/6e9b346d-2ec9-4148-ae62-7087797d3188
|
|
|
52
52
|
| `/topics onboard` | 交互式配置向导:唤起 dsh 原生 ask-user 面板逐项问答(模式 / 仓库 / 蒸馏 / 注入档位 / 自动观察),末步确认才写入;无 ask-user UI 的环境自动退化为逐条输入 |
|
|
53
53
|
| `/topics status` | bundle 健康:topic 数、观察积压、冲突、最近蒸馏结果、同步状态 |
|
|
54
54
|
| `/topics distill` | 手动触发一次蒸馏 run(复用现有 lane 与 in-flight 守卫;输出摘要与 distill-state 字段一一对应) |
|
|
55
|
+
| `/topics consolidate` | 手动触发一次整理 run:LLM 园丁合并重复/晋升 stable/废弃过时/刷新元数据,逐条 git commit 可回滚 |
|
|
55
56
|
| `/topics stats` | 注入统计:hit rate、top-N、near-miss 分布与调参建议 |
|
|
56
57
|
| `/topics list` / `show` / `history` | 浏览 Topic、反向引用与变更史 |
|
|
57
58
|
| `/topics graph` | 生成关系图网页(力导向、可拖拽缩放、悬停看结论)并自动在浏览器打开 |
|
|
@@ -121,6 +122,9 @@ dsh plugin --profile <name> remove @aiwayds/dsh-topics-memory
|
|
|
121
122
|
| `distillOnSessionEnd` | `true` | session 结束时蒸馏一次 |
|
|
122
123
|
| `distillBatchSize` | `40` | 每次蒸馏模型调用携带的观察条数。遇输出上限(`max-tokens`)失败自动减半重试(下限 5),失败批次不再活锁积压;缩小状态跨 run 保持,直到插件重载或配置变更。注意:`/topics set distillBatchSize` 重设为同值不触发复位,需设为不同值或重载插件 |
|
|
123
124
|
| `distillMaxModelCalls` | `8` | 单次蒸馏 run 的模型调用预算,含 ops 未回显有效 `observed_ids` 时的纠错重试(至多一次,预算放不下即零消费停机)。预算耗尽即停,已成功的批次照常 markDistilled(部分前进优于零前进),distill state 记 `partial: …` |
|
|
125
|
+
| `consolidateCadence` | `daily` | 整理 lane 节拍:`daily`/`3d`/`7d`/`off`。会话启动时检查上次整理时间(`meta/consolidate-state.json`),到期即在后台自动跑 LLM 园丁(复用蒸馏模型路由):本地词面聚类只送「长得像」的候选簇,四类动作 merge/promote/deprecate/refresh,越权 op 一律丢弃;模型调用失败不推进节拍,下次启动重试 |
|
|
126
|
+
| `deprecatedTtlDays` | `15` | deprecated 条目超过 N 天在会话启动时自动删除(本地规则不依赖模型,逐条 git commit 可回溯找回);`0` 关闭清扫 |
|
|
127
|
+
| `usageBoost` | `0.15` | 使用加成(ADR 0015):近 30 天被注入命中/点开过的 Topic 检索加分(计入门槛分、帽 0.2、零词面相关不加、结构门不豁免);`0` 关闭 |
|
|
124
128
|
| `pushDebounceSeconds` | `45` | GitHub 模式去抖推送间隔 |
|
|
125
129
|
|
|
126
130
|
## Acknowledgements
|
package/lib/commands.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { CommandDefinition, CommandInvocation } from '@deepseek-ai/dsh-commands';
|
|
9
9
|
import type { TopicsService } from './service.ts';
|
|
10
10
|
import type { DistillResult } from './distill.ts';
|
|
11
|
+
import type { ConsolidateResult } from './consolidate.ts';
|
|
11
12
|
import { aggregateStats } from './ilog.ts';
|
|
12
13
|
import { type AskServiceResolver, type LlmDirectoryResolver, type MutateFn } from './onboard.ts';
|
|
13
14
|
/**
|
|
@@ -16,6 +17,12 @@ import { type AskServiceResolver, type LlmDirectoryResolver, type MutateFn } fro
|
|
|
16
17
|
* the command's normal error path.
|
|
17
18
|
*/
|
|
18
19
|
export type ManualDistill = (invocation: CommandInvocation) => Promise<DistillResult>;
|
|
20
|
+
/**
|
|
21
|
+
* Manual `/topics consolidate` trigger, wired by the host (index.ts owns the
|
|
22
|
+
* lane instance). Bypasses the cadence gate; the lane's own in-flight guard
|
|
23
|
+
* still applies.
|
|
24
|
+
*/
|
|
25
|
+
export type ManualConsolidate = (invocation: CommandInvocation) => Promise<ConsolidateResult>;
|
|
19
26
|
export declare const HELP: string;
|
|
20
|
-
export declare function buildTopicsCommand(service: TopicsService, mutate: MutateFn, resolveAsk?: AskServiceResolver, resolveLlm?: LlmDirectoryResolver, distillNow?: ManualDistill): CommandDefinition;
|
|
27
|
+
export declare function buildTopicsCommand(service: TopicsService, mutate: MutateFn, resolveAsk?: AskServiceResolver, resolveLlm?: LlmDirectoryResolver, distillNow?: ManualDistill, consolidateNow?: ManualConsolidate): CommandDefinition;
|
|
21
28
|
export declare function tuningHint(stats: ReturnType<typeof aggregateStats>, threshold: number): string | undefined;
|
package/lib/commands.js
CHANGED
|
@@ -18,6 +18,7 @@ export const HELP = [
|
|
|
18
18
|
' /topics onboard 交互式配置向导(ask-user 面板逐项问答;无 UI 环境逐条输入)',
|
|
19
19
|
' /topics status bundle 健康:topic 数、观察积压、冲突、同步状态',
|
|
20
20
|
' /topics distill 手动触发一次蒸馏(观察池 → Topic,输出 marked/created/updated/gc 摘要)',
|
|
21
|
+
' /topics consolidate 手动触发一次整理(合并重复/晋升 stable/废弃过时/刷新元数据,无视 cadence 立即跑)',
|
|
21
22
|
' /topics stats 注入统计:hit rate、top-N、near-miss 分布与调参建议',
|
|
22
23
|
' /topics list 列出全部 Topic',
|
|
23
24
|
' /topics show <slug> 查看一个 Topic 全文(含反向引用)',
|
|
@@ -30,16 +31,16 @@ export const HELP = [
|
|
|
30
31
|
'',
|
|
31
32
|
'凭据:$GITHUB_TOKEN 或已登录的 gh CLI;登录不在本插件职责内。',
|
|
32
33
|
].join('\n');
|
|
33
|
-
export function buildTopicsCommand(service, mutate, resolveAsk = () => undefined, resolveLlm = () => [], distillNow) {
|
|
34
|
+
export function buildTopicsCommand(service, mutate, resolveAsk = () => undefined, resolveLlm = () => [], distillNow, consolidateNow) {
|
|
34
35
|
const onboard = createOnboardHandler(service, mutate, undefined, resolveAsk, resolveLlm);
|
|
35
36
|
return {
|
|
36
37
|
name: 'topics',
|
|
37
|
-
description: 'OKF topic 记忆:onboard | distill | status | stats | list | show | history | graph | sync | config | set',
|
|
38
|
-
input: { hint: '[onboard | distill | status | stats | list | show <slug> | history <slug> | graph | sync [pull|push] | config | set <key> <value>]' },
|
|
39
|
-
handler: (invocation) => handle(invocation, service, mutate, onboard, resolveAsk, resolveLlm, distillNow),
|
|
38
|
+
description: 'OKF topic 记忆:onboard | distill | consolidate | status | stats | list | show | history | graph | sync | config | set',
|
|
39
|
+
input: { hint: '[onboard | distill | consolidate | status | stats | list | show <slug> | history <slug> | graph | sync [pull|push] | config | set <key> <value>]' },
|
|
40
|
+
handler: (invocation) => handle(invocation, service, mutate, onboard, resolveAsk, resolveLlm, distillNow, consolidateNow),
|
|
40
41
|
};
|
|
41
42
|
}
|
|
42
|
-
async function handle(invocation, service, mutate, onboard, resolveAsk, resolveLlm, distillNow) {
|
|
43
|
+
async function handle(invocation, service, mutate, onboard, resolveAsk, resolveLlm, distillNow, consolidateNow) {
|
|
43
44
|
const raw = invocation.rawInput.trim();
|
|
44
45
|
const [action = '', ...rest] = raw.split(/\s+/);
|
|
45
46
|
try {
|
|
@@ -52,6 +53,8 @@ async function handle(invocation, service, mutate, onboard, resolveAsk, resolveL
|
|
|
52
53
|
return ok(await renderStatus(service));
|
|
53
54
|
case 'distill':
|
|
54
55
|
return await doDistill(service, invocation, distillNow);
|
|
56
|
+
case 'consolidate':
|
|
57
|
+
return await doConsolidate(service, invocation, consolidateNow);
|
|
55
58
|
case 'stats':
|
|
56
59
|
return ok(await renderStats(service));
|
|
57
60
|
case 'list':
|
|
@@ -97,6 +100,7 @@ async function renderStatus(service) {
|
|
|
97
100
|
['注入去重', cfg.injectDedup ? '开(同会话已注入的 Topic 不重注)' : '关'],
|
|
98
101
|
['慢道', cfg.qualityLane === 'off' ? '关' : cfg.qualityLane === 'always' ? `每轮(${cfg.distillProvider}/${cfg.distillModel})` : `采样 1/3(${cfg.distillProvider}/${cfg.distillModel})`],
|
|
99
102
|
['蒸馏', cfg.distillProvider !== '' && cfg.distillModel !== '' ? `${cfg.distillProvider}/${cfg.distillModel},每 ${cfg.distillEveryTurns} 轮` : '未配置模型(/topics set distill-provider / distill-model)'],
|
|
103
|
+
['整理', cadenceLabel(cfg.consolidateCadence) + (cfg.consolidateCadence === 'off' ? '' : `(复用蒸馏模型,/topics consolidate 立即跑)`)],
|
|
100
104
|
];
|
|
101
105
|
// Last lane outcome (distill-state summary) — what "checkable via
|
|
102
106
|
// /topics status" promises; absent until the first run of this bundle.
|
|
@@ -108,6 +112,20 @@ async function renderStatus(service) {
|
|
|
108
112
|
const gc = typeof lastRun.gcDropped === 'number' && lastRun.gcDropped > 0 ? `,GC 回收 ${lastRun.gcDropped}` : '';
|
|
109
113
|
rows.push(['最近蒸馏', `${outcome}${gc} @ ${cell(String(lastRun.at ?? '').slice(0, 19).replace('T', ' '))}`]);
|
|
110
114
|
}
|
|
115
|
+
const lastConsolidate = await service.store.readConsolidateState();
|
|
116
|
+
if (lastConsolidate !== undefined) {
|
|
117
|
+
const count = (v) => (Array.isArray(v) ? v.length : 0);
|
|
118
|
+
const counts = [
|
|
119
|
+
`合并 ${count(lastConsolidate.merged)}`,
|
|
120
|
+
`晋升 ${count(lastConsolidate.promoted)}`,
|
|
121
|
+
`废弃 ${count(lastConsolidate.deprecated)}`,
|
|
122
|
+
`刷新 ${count(lastConsolidate.refreshed)}`,
|
|
123
|
+
].join('/');
|
|
124
|
+
rows.push([
|
|
125
|
+
'最近整理',
|
|
126
|
+
`${counts} @ ${cell(String(lastConsolidate.at ?? '').slice(0, 19).replace('T', ' '))}${lastConsolidate.ok === false && lastConsolidate.detail ? `(${String(lastConsolidate.detail).slice(0, 80)})` : ''}`,
|
|
127
|
+
]);
|
|
128
|
+
}
|
|
111
129
|
if (service.sync !== undefined) {
|
|
112
130
|
rows.push(['上次推送', cell(String(service.sync.lastPushAt ?? '从未'))]);
|
|
113
131
|
if (service.sync.lastError !== '')
|
|
@@ -127,6 +145,58 @@ async function renderStatus(service) {
|
|
|
127
145
|
* summary mirrors the distill state fields (ok / marked / created / updated /
|
|
128
146
|
* gc dropped / reason) so what the user sees is what the state file records.
|
|
129
147
|
*/
|
|
148
|
+
/** Human-readable cadence for status/consolidate output. */
|
|
149
|
+
function cadenceLabel(value) {
|
|
150
|
+
switch (value) {
|
|
151
|
+
case 'daily':
|
|
152
|
+
return '每天';
|
|
153
|
+
case '3d':
|
|
154
|
+
return '每 3 天';
|
|
155
|
+
case '7d':
|
|
156
|
+
return '每 7 天';
|
|
157
|
+
default:
|
|
158
|
+
return '关';
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* `/topics consolidate` — one manual run of the consolidation lane (LLM
|
|
163
|
+
* gardener over the existing pool). Bypasses the cadence gate; the lane's
|
|
164
|
+
* own in-flight guard dedups a concurrent automatic run. Output mirrors the
|
|
165
|
+
* lane's actions with per-op reasons so the user can eyeball (and git-revert)
|
|
166
|
+
* what the model decided.
|
|
167
|
+
*/
|
|
168
|
+
async function doConsolidate(service, invocation, consolidateNow) {
|
|
169
|
+
if (consolidateNow === undefined)
|
|
170
|
+
return fail('整理 lane 未接线(宿主未提供手动整理触发器)');
|
|
171
|
+
const cfg = service.cfg;
|
|
172
|
+
if (cfg.distillProvider === '' || cfg.distillModel === '') {
|
|
173
|
+
return fail('整理复用蒸馏模型路由:请先 /topics set distill-provider 与 distill-model。');
|
|
174
|
+
}
|
|
175
|
+
const result = await consolidateNow(invocation);
|
|
176
|
+
if (!result.ok) {
|
|
177
|
+
const reasonNote = result.reason === 'no-clusters'
|
|
178
|
+
? '没有找到可能相近的 topic 候选簇(词面上没有疑似重复/相关对)'
|
|
179
|
+
: result.reason === 'no-model'
|
|
180
|
+
? '蒸馏模型未配置(/topics set distill-provider / distill-model)'
|
|
181
|
+
: result.reason === 'in-flight'
|
|
182
|
+
? '已有整理在跑,稍后再试'
|
|
183
|
+
: (result.reason ?? 'unknown');
|
|
184
|
+
return fail(`整理未执行:${reasonNote}${result.detail !== undefined ? `\n ${result.detail}` : ''}`);
|
|
185
|
+
}
|
|
186
|
+
const lines = [];
|
|
187
|
+
const actions = result.actions ?? [];
|
|
188
|
+
const KIND_LABEL = { merge: '合并', promote: '晋升', deprecate: '废弃', refresh: '刷新' };
|
|
189
|
+
for (const a of actions) {
|
|
190
|
+
if (a.kind === 'merge' && a.merged !== undefined) {
|
|
191
|
+
lines.push(`- 合并 ${a.merged.map((m) => cell(m)).join('、')} → ${cell(a.slug)}${a.reason !== undefined ? `:${a.reason}` : ''}`);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
lines.push(`- ${KIND_LABEL[a.kind] ?? a.kind} ${cell(a.slug)}${a.reason !== undefined ? `:${a.reason}` : ''}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
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}` : ''}`;
|
|
198
|
+
return ok(lines.length === 0 ? header : `${header}\n${lines.join('\n')}\n全部变更已逐条 git commit,可 /topics history <slug> 追溯、git revert 回滚。`);
|
|
199
|
+
}
|
|
130
200
|
async function doDistill(service, invocation, distillNow) {
|
|
131
201
|
if (distillNow === undefined)
|
|
132
202
|
return fail('蒸馏 lane 未接线(宿主未提供手动蒸馏触发器)');
|
package/lib/config.d.ts
CHANGED
|
@@ -56,6 +56,20 @@ export declare const TopicsConfig: z<Schemastery.ObjectS<{
|
|
|
56
56
|
* Default 8: with the batch loop, one run then drains ~30-40 observations
|
|
57
57
|
* instead of ~10, which is what makes a real backlog actually shrink. */
|
|
58
58
|
distillMaxModelCalls: z<number, number>;
|
|
59
|
+
/** Consolidation lane (整理) cadence: how often a session start may run the
|
|
60
|
+
* LLM gardener over the EXISTING pool (merge near-duplicates, promote
|
|
61
|
+
* settled drafts, deprecate superseded, refresh metadata). Reuses the
|
|
62
|
+
* distill model route; off disables the lane entirely. */
|
|
63
|
+
consolidateCadence: z<string, string>;
|
|
64
|
+
/** Deprecated topics older than this many days are dropped at session start
|
|
65
|
+
* (local TTL sweep, no model; each drop is its own git commit, so history
|
|
66
|
+
* stays recoverable on the remote). 0 disables the sweep. */
|
|
67
|
+
deprecatedTtlDays: z<number, number>;
|
|
68
|
+
/** UsageBoost (ADR 0015): behavioral bonus for topics injected/opened in
|
|
69
|
+
* the last 30 days, folded into the gate score — capped at 0.2 and never
|
|
70
|
+
* granted to zero-lexical candidates; the structural gate still applies.
|
|
71
|
+
* 0 = off. */
|
|
72
|
+
usageBoost: z<number, number>;
|
|
59
73
|
/** Debounced push delay in GitHub mode. */
|
|
60
74
|
pushDebounceSeconds: z<number, number>;
|
|
61
75
|
}>, Schemastery.ObjectT<{
|
|
@@ -109,6 +123,20 @@ export declare const TopicsConfig: z<Schemastery.ObjectS<{
|
|
|
109
123
|
* Default 8: with the batch loop, one run then drains ~30-40 observations
|
|
110
124
|
* instead of ~10, which is what makes a real backlog actually shrink. */
|
|
111
125
|
distillMaxModelCalls: z<number, number>;
|
|
126
|
+
/** Consolidation lane (整理) cadence: how often a session start may run the
|
|
127
|
+
* LLM gardener over the EXISTING pool (merge near-duplicates, promote
|
|
128
|
+
* settled drafts, deprecate superseded, refresh metadata). Reuses the
|
|
129
|
+
* distill model route; off disables the lane entirely. */
|
|
130
|
+
consolidateCadence: z<string, string>;
|
|
131
|
+
/** Deprecated topics older than this many days are dropped at session start
|
|
132
|
+
* (local TTL sweep, no model; each drop is its own git commit, so history
|
|
133
|
+
* stays recoverable on the remote). 0 disables the sweep. */
|
|
134
|
+
deprecatedTtlDays: z<number, number>;
|
|
135
|
+
/** UsageBoost (ADR 0015): behavioral bonus for topics injected/opened in
|
|
136
|
+
* the last 30 days, folded into the gate score — capped at 0.2 and never
|
|
137
|
+
* granted to zero-lexical candidates; the structural gate still applies.
|
|
138
|
+
* 0 = off. */
|
|
139
|
+
usageBoost: z<number, number>;
|
|
112
140
|
/** Debounced push delay in GitHub mode. */
|
|
113
141
|
pushDebounceSeconds: z<number, number>;
|
|
114
142
|
}>>;
|
|
@@ -135,9 +163,12 @@ export type TopicsConfigValue = {
|
|
|
135
163
|
distillModel: string;
|
|
136
164
|
distillBatchSize: number;
|
|
137
165
|
distillMaxModelCalls: number;
|
|
166
|
+
consolidateCadence: string;
|
|
167
|
+
deprecatedTtlDays: number;
|
|
168
|
+
usageBoost: number;
|
|
138
169
|
pushDebounceSeconds: number;
|
|
139
170
|
};
|
|
140
|
-
export declare const CONFIG_KEYS: readonly ["repo", "autoInject", "injectDedup", "suppressEcho", "topK", "perTopicBudget", "totalBudget", "matchThreshold", "tagBoost", "injectMode", "qualityLane", "graphDepth", "recencyWindowDays", "autoObserve", "includeSubagents", "observationMaxChars", "distillEveryTurns", "distillOnSessionEnd", "distillProvider", "distillModel", "distillBatchSize", "distillMaxModelCalls", "pushDebounceSeconds"];
|
|
171
|
+
export declare const CONFIG_KEYS: readonly ["repo", "autoInject", "injectDedup", "suppressEcho", "topK", "perTopicBudget", "totalBudget", "matchThreshold", "tagBoost", "injectMode", "qualityLane", "graphDepth", "recencyWindowDays", "autoObserve", "includeSubagents", "observationMaxChars", "distillEveryTurns", "distillOnSessionEnd", "distillProvider", "distillModel", "distillBatchSize", "distillMaxModelCalls", "consolidateCadence", "deprecatedTtlDays", "usageBoost", "pushDebounceSeconds"];
|
|
141
172
|
export type ConfigKey = (typeof CONFIG_KEYS)[number];
|
|
142
173
|
/** CamelCase → dash-display (topK → top-k) for command output. */
|
|
143
174
|
export declare function displayKey(key: string): string;
|
package/lib/config.js
CHANGED
|
@@ -56,6 +56,20 @@ export const TopicsConfig = z.object({
|
|
|
56
56
|
* Default 8: with the batch loop, one run then drains ~30-40 observations
|
|
57
57
|
* instead of ~10, which is what makes a real backlog actually shrink. */
|
|
58
58
|
distillMaxModelCalls: z.number().default(8),
|
|
59
|
+
/** Consolidation lane (整理) cadence: how often a session start may run the
|
|
60
|
+
* LLM gardener over the EXISTING pool (merge near-duplicates, promote
|
|
61
|
+
* settled drafts, deprecate superseded, refresh metadata). Reuses the
|
|
62
|
+
* distill model route; off disables the lane entirely. */
|
|
63
|
+
consolidateCadence: z.string().default('daily'),
|
|
64
|
+
/** Deprecated topics older than this many days are dropped at session start
|
|
65
|
+
* (local TTL sweep, no model; each drop is its own git commit, so history
|
|
66
|
+
* stays recoverable on the remote). 0 disables the sweep. */
|
|
67
|
+
deprecatedTtlDays: z.number().default(15),
|
|
68
|
+
/** UsageBoost (ADR 0015): behavioral bonus for topics injected/opened in
|
|
69
|
+
* the last 30 days, folded into the gate score — capped at 0.2 and never
|
|
70
|
+
* granted to zero-lexical candidates; the structural gate still applies.
|
|
71
|
+
* 0 = off. */
|
|
72
|
+
usageBoost: z.number().default(0.15),
|
|
59
73
|
/** Debounced push delay in GitHub mode. */
|
|
60
74
|
pushDebounceSeconds: z.number().default(45),
|
|
61
75
|
});
|
|
@@ -82,6 +96,9 @@ export const CONFIG_KEYS = [
|
|
|
82
96
|
'distillModel',
|
|
83
97
|
'distillBatchSize',
|
|
84
98
|
'distillMaxModelCalls',
|
|
99
|
+
'consolidateCadence',
|
|
100
|
+
'deprecatedTtlDays',
|
|
101
|
+
'usageBoost',
|
|
85
102
|
'pushDebounceSeconds',
|
|
86
103
|
];
|
|
87
104
|
/** CamelCase → dash-display (topK → top-k) for command output. */
|
|
@@ -118,6 +135,7 @@ export function parseConfigValue(key, raw) {
|
|
|
118
135
|
case 'distillEveryTurns':
|
|
119
136
|
case 'distillBatchSize':
|
|
120
137
|
case 'distillMaxModelCalls':
|
|
138
|
+
case 'deprecatedTtlDays':
|
|
121
139
|
case 'pushDebounceSeconds': {
|
|
122
140
|
const n = Number(raw);
|
|
123
141
|
if (!Number.isFinite(n) || n < 0 || !Number.isInteger(n))
|
|
@@ -129,13 +147,19 @@ export function parseConfigValue(key, raw) {
|
|
|
129
147
|
return raw;
|
|
130
148
|
return { error: 'inject-mode 取值 pointer|digest' };
|
|
131
149
|
}
|
|
150
|
+
case 'consolidateCadence': {
|
|
151
|
+
if (raw === 'off' || raw === 'daily' || raw === '3d' || raw === '7d')
|
|
152
|
+
return raw;
|
|
153
|
+
return { error: 'consolidate-cadence 取值 off|daily|3d|7d' };
|
|
154
|
+
}
|
|
132
155
|
case 'qualityLane': {
|
|
133
156
|
if (raw === 'off' || raw === 'sampled' || raw === 'always')
|
|
134
157
|
return raw;
|
|
135
158
|
return { error: 'quality-lane 取值 off|sampled|always' };
|
|
136
159
|
}
|
|
137
160
|
case 'matchThreshold':
|
|
138
|
-
case 'tagBoost':
|
|
161
|
+
case 'tagBoost':
|
|
162
|
+
case 'usageBoost': {
|
|
139
163
|
const n = Number(raw);
|
|
140
164
|
if (!Number.isFinite(n) || n < 0)
|
|
141
165
|
return { error: `${key} 需要非负数` };
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consolidation lane — periodic LLM gardening over the topic pool itself.
|
|
3
|
+
*
|
|
4
|
+
* Where the distill lane turns observations into topics, this lane turns the
|
|
5
|
+
* EXISTING pool into a better one: merging near-duplicates, promoting drafts
|
|
6
|
+
* whose conclusions have settled, deprecating superseded entries, refreshing
|
|
7
|
+
* metadata (title/tags/triggers). Triggered by cadence at session start
|
|
8
|
+
* (`consolidate-cadence`: off | daily | 3d | 7d) or manually via
|
|
9
|
+
* `/topics consolidate`. The model route is shared with the distill lane
|
|
10
|
+
* (distill-provider/distill-model) — one route to configure, one caller
|
|
11
|
+
* instance injected.
|
|
12
|
+
*
|
|
13
|
+
* Safety model:
|
|
14
|
+
* - grounded proposals: clusters are built locally (lexical similarity over
|
|
15
|
+
* title+tags tokens), so the model only ever judges topics that already
|
|
16
|
+
* look alike and every op names its slugs verbatim from the bundle;
|
|
17
|
+
* - four op kinds only, and refresh may NOT touch the conclusion body —
|
|
18
|
+
* rewriting conclusions is merge's exclusive path, so a metadata refresh
|
|
19
|
+
* can never silently lose knowledge; no create: consolidation prunes,
|
|
20
|
+
* it never grows;
|
|
21
|
+
* - every write rides the normal saveTopic path (one git commit per change),
|
|
22
|
+
* so the whole run is one `git revert` away;
|
|
23
|
+
* - the cadence stamp (meta/consolidate-state.json) advances only when the
|
|
24
|
+
* model actually evaluated at least one cluster — a run that died on the
|
|
25
|
+
* first call leaves the stamp alone, and the next session start retries.
|
|
26
|
+
*
|
|
27
|
+
* @module consolidate
|
|
28
|
+
*/
|
|
29
|
+
import type { TopicsService } from './service.ts';
|
|
30
|
+
import { type ModelCaller } from './distill.ts';
|
|
31
|
+
/** Clusters are capped — a giant cluster drowns the model in candidates. */
|
|
32
|
+
export declare const MAX_CLUSTER_SIZE = 6;
|
|
33
|
+
export interface ConsolidateOp {
|
|
34
|
+
op: 'merge' | 'promote' | 'deprecate' | 'refresh';
|
|
35
|
+
/** promote/deprecate/refresh target. */
|
|
36
|
+
slug?: string;
|
|
37
|
+
/** merge: the surviving slug. */
|
|
38
|
+
survivor?: string;
|
|
39
|
+
/** merge: slugs absorbed INTO the survivor (≥ 1). */
|
|
40
|
+
merged?: string[];
|
|
41
|
+
title?: string;
|
|
42
|
+
description?: string;
|
|
43
|
+
tags?: string[];
|
|
44
|
+
triggers?: string[];
|
|
45
|
+
/** merge only (refresh may not rewrite conclusions). */
|
|
46
|
+
conclusion?: string;
|
|
47
|
+
reason?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface ConsolidateAction {
|
|
50
|
+
kind: ConsolidateOp['op'];
|
|
51
|
+
/** merge: survivor; others: the target slug. */
|
|
52
|
+
slug: string;
|
|
53
|
+
/** merge: the absorbed slugs. */
|
|
54
|
+
merged?: string[];
|
|
55
|
+
reason?: string;
|
|
56
|
+
}
|
|
57
|
+
export interface ConsolidateResult {
|
|
58
|
+
ok: boolean;
|
|
59
|
+
reason?: 'no-model' | 'no-clusters' | 'in-flight' | 'model-error' | 'invalid-output';
|
|
60
|
+
merged: string[];
|
|
61
|
+
promoted: string[];
|
|
62
|
+
deprecated: string[];
|
|
63
|
+
refreshed: string[];
|
|
64
|
+
/** Ops rejected by sanitization, with the model's raw op count for context. */
|
|
65
|
+
droppedOps?: number;
|
|
66
|
+
rawOps?: number;
|
|
67
|
+
calls?: number;
|
|
68
|
+
actions?: ConsolidateAction[];
|
|
69
|
+
detail?: string;
|
|
70
|
+
}
|
|
71
|
+
export declare const CONSOLIDATE_SYSTEM_PROMPT: string;
|
|
72
|
+
/** Latin words + CJK bigrams — the token space title/tags similarity lives in. */
|
|
73
|
+
export declare function topicTokens(text: string): Set<string>;
|
|
74
|
+
export interface ClusterEntry {
|
|
75
|
+
slug: string;
|
|
76
|
+
title: string;
|
|
77
|
+
status: string;
|
|
78
|
+
tags: readonly string[];
|
|
79
|
+
description?: string;
|
|
80
|
+
conclusion: string;
|
|
81
|
+
}
|
|
82
|
+
export interface TopicCluster {
|
|
83
|
+
entries: ClusterEntry[];
|
|
84
|
+
/** Peak pairwise similarity inside the cluster — runs process peaks first. */
|
|
85
|
+
similarity: number;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Group entries whose title+description+tags tokens clear the pairwise
|
|
89
|
+
* threshold (union-find over similar pairs). Singletons are dropped — the
|
|
90
|
+
* model only ever sees topics that already look alike. Clusters larger than
|
|
91
|
+
* {@link MAX_CLUSTER_SIZE} keep their most-connected members.
|
|
92
|
+
*/
|
|
93
|
+
export declare function clusterTopics(entries: readonly ClusterEntry[], threshold?: number): TopicCluster[];
|
|
94
|
+
/**
|
|
95
|
+
* Drop `deprecated` topics older than `ttlDays` — pure housekeeping, never
|
|
96
|
+
* touches the model. `generated.at` is the right clock: every save restamps
|
|
97
|
+
* it and nothing ever touches a deprecated entry afterwards, so it IS the
|
|
98
|
+
* "became deprecated at" timestamp. Deletion rides the normal store path
|
|
99
|
+
* (one commit per drop, index regenerated), so the history stays recoverable
|
|
100
|
+
* on the remote; an unparsable stamp is skipped, not guessed. Returns the
|
|
101
|
+
* dropped slugs. `ttlDays <= 0` disables the sweep.
|
|
102
|
+
*/
|
|
103
|
+
export declare function dropExpiredDeprecated(service: TopicsService, ttlDays: number, now?: number): Promise<string[]>;
|
|
104
|
+
export declare class Consolidator {
|
|
105
|
+
private inFlight;
|
|
106
|
+
private readonly service;
|
|
107
|
+
private readonly caller;
|
|
108
|
+
constructor(service: TopicsService, caller: ModelCaller | undefined);
|
|
109
|
+
get configured(): boolean;
|
|
110
|
+
/** Cadence days for a config value; undefined = off (or unrecognized). */
|
|
111
|
+
static cadenceDays(value: string | undefined): number | undefined;
|
|
112
|
+
get hasPending(): boolean;
|
|
113
|
+
/**
|
|
114
|
+
* Cadence-gated entry (session start): silently skips when off, not due,
|
|
115
|
+
* or already running. Due = no stamp yet (first run) or the last EVALUATED
|
|
116
|
+
* run is older than the cadence.
|
|
117
|
+
*/
|
|
118
|
+
maybeRun(opts?: {
|
|
119
|
+
sessionId?: string;
|
|
120
|
+
now?: number;
|
|
121
|
+
}): Promise<ConsolidateResult | undefined>;
|
|
122
|
+
/** Manual entry (/topics consolidate): bypasses the cadence, keeps the guard. */
|
|
123
|
+
run(sessionId?: string): Promise<ConsolidateResult>;
|
|
124
|
+
private start;
|
|
125
|
+
private runInner;
|
|
126
|
+
/** Read the active pool and cluster it lexically. */
|
|
127
|
+
private buildClusters;
|
|
128
|
+
/** One model call over one cluster, ops shape-validated but not yet applied. */
|
|
129
|
+
private runCluster;
|
|
130
|
+
/**
|
|
131
|
+
* Validate and apply ops against the live bundle. Slugs are slugify-normalized
|
|
132
|
+
* and must exist; merge requires a non-empty conclusion and forbids self-merge;
|
|
133
|
+
* refresh never receives the conclusion field (merge's exclusive path); ops on
|
|
134
|
+
* slugs already merged away earlier in this run are skipped (the deprecation
|
|
135
|
+
* already redirects readers to the survivor). One bad op never sinks the rest.
|
|
136
|
+
*/
|
|
137
|
+
private applyOps;
|
|
138
|
+
}
|