@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/lib/index.js
CHANGED
|
@@ -30,6 +30,7 @@ import { buildTopicTools } from "./tools.js";
|
|
|
30
30
|
import { buildTopicsCommand } from "./commands.js";
|
|
31
31
|
import { Observer, textOf } from "./observer.js";
|
|
32
32
|
import { Distiller, defaultModelCaller } from "./distill.js";
|
|
33
|
+
import { Consolidator, dropExpiredDeprecated } from "./consolidate.js";
|
|
33
34
|
import { SlowLane } from "./quality.js";
|
|
34
35
|
import { CONFIG_KEYS, TopicsConfig } from "./config.js";
|
|
35
36
|
import { isDelegated } from "./delegation.js";
|
|
@@ -126,17 +127,17 @@ export function migrateLegacySettings(settings, warn) {
|
|
|
126
127
|
}
|
|
127
128
|
// --- Bundled skill -----------------------------------------------------------
|
|
128
129
|
/** Provider name under `ctx.skills`; doubles as the skill name. */
|
|
129
|
-
const SKILL_PROVIDER_NAME = 'dsh-topics-memory';
|
|
130
|
+
const SKILL_PROVIDER_NAME = 'dsh-topics-memory-config';
|
|
130
131
|
/** Packaged skill body; `../skills/` resolves to the package root from both lib/ and src/. */
|
|
131
|
-
const SKILL_BODY_URL = new URL('../skills/dsh-topics-memory/SKILL.md', import.meta.url);
|
|
132
|
+
const SKILL_BODY_URL = new URL('../skills/dsh-topics-memory-config/SKILL.md', import.meta.url);
|
|
132
133
|
/** Resource base served with the skill so its relative links resolve. */
|
|
133
134
|
const SKILL_RESOURCE_BASE = {
|
|
134
135
|
kind: 'directory',
|
|
135
|
-
path: fileURLToPath(new URL('../skills/dsh-topics-memory/', import.meta.url)),
|
|
136
|
+
path: fileURLToPath(new URL('../skills/dsh-topics-memory-config/', import.meta.url)),
|
|
136
137
|
};
|
|
137
138
|
const SKILL_INVOCATION = { modelInvocable: true, userInvocable: true };
|
|
138
139
|
/** Routing description; must stay identical to the SKILL.md frontmatter (asserted in tests). */
|
|
139
|
-
const SKILL_DESCRIPTION = 'dsh 记忆插件(@aiwayds/dsh-topics-memory)使用与配置指南。凡涉及 dsh 记忆/话题库/GitHub
|
|
140
|
+
const SKILL_DESCRIPTION = 'dsh 记忆插件(@aiwayds/dsh-topics-memory)使用与配置指南。凡涉及 dsh 记忆/话题库/GitHub 同步/蒸馏/整理,或要配置 topics 段时先读本指南:settings.yaml 顶层 `topics:` 段全部键(repo/autoInject/topK/注入预算/蒸馏/整理/观察/图游走等)、/topics 命令族(onboard/status/distill/consolidate/stats/list/show/history/graph/sync/config/set)、首次配置 ask_user_question 向导(local-only 或绑 GitHub 仓、蒸馏模型路由、注入档位、自动观察)、注入形态 pointer/digest、legacy llmwiki 段自动迁移。触发词:topics、记忆、topic、蒸馏、distill、整理、consolidate、合并重复、deprecatedTtl、usageBoost、使用加成、autoInject、记忆库、llmwiki、include-subagents。';
|
|
140
141
|
const SKILL_CANDIDATE = {
|
|
141
142
|
name: SKILL_PROVIDER_NAME,
|
|
142
143
|
description: SKILL_DESCRIPTION,
|
|
@@ -410,6 +411,10 @@ export function apply(ctx) {
|
|
|
410
411
|
// The settle hook re-runs the release check because a disposal that landed
|
|
411
412
|
// mid-pipeline deferred to THIS pipeline and never fires again.
|
|
412
413
|
const slowLane = new SlowLane(service, caller, releaseSessionLlm);
|
|
414
|
+
// Consolidation lane (整理): same caller/route as distill, global single
|
|
415
|
+
// flight inside. Reads the pool and merges/promotes/deprecates/refreshes;
|
|
416
|
+
// cadence-gated at session start, manual via /topics consolidate.
|
|
417
|
+
const consolidator = new Consolidator(service, caller);
|
|
413
418
|
// ---- Observer (M2) ----
|
|
414
419
|
// Trigger runs are fire-and-forget everywhere now — including the exit
|
|
415
420
|
// one. Observations are write-through on disk, so a run the process exits
|
|
@@ -586,6 +591,39 @@ export function apply(ctx) {
|
|
|
586
591
|
void run.catch(() => undefined);
|
|
587
592
|
})
|
|
588
593
|
.catch(() => undefined);
|
|
594
|
+
// Consolidation cadence check — after the pull (freshest pool),
|
|
595
|
+
// fully fire-and-forget: cadence/single-flight gates live inside
|
|
596
|
+
// maybeRun, and a no-model boot just skips (no stamp advanced).
|
|
597
|
+
// Failures are NOT silent at the host log: the 2026-09-09 real-host
|
|
598
|
+
// test showed a dead distill route would otherwise hide here with
|
|
599
|
+
// zero observable trace (the state file only advances on success).
|
|
600
|
+
try {
|
|
601
|
+
captureFromAgent(agents()?.get(session.id));
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
// contained — the run below fails with a readable no-model detail
|
|
605
|
+
}
|
|
606
|
+
void consolidator
|
|
607
|
+
.maybeRun({ sessionId: String(session.id) })
|
|
608
|
+
.then((r) => {
|
|
609
|
+
if (r !== undefined && !r.ok && r.reason !== 'no-clusters') {
|
|
610
|
+
warn(`dsh-topics-memory 整理 lane 未执行:${r.reason ?? 'unknown'}${r.detail !== undefined ? `(${r.detail})` : ''}`);
|
|
611
|
+
}
|
|
612
|
+
})
|
|
613
|
+
.catch(() => undefined);
|
|
614
|
+
// Deprecated-TTL sweep — pure local rule, no model, runs even when
|
|
615
|
+
// the distill route is unconfigured. Drops are logged: deletion is
|
|
616
|
+
// the one housekeeping action the user should always see happened.
|
|
617
|
+
const ttl = cfgNow().deprecatedTtlDays;
|
|
618
|
+
if (ttl > 0) {
|
|
619
|
+
void dropExpiredDeprecated(service, ttl)
|
|
620
|
+
.then((dropped) => {
|
|
621
|
+
if (dropped.length > 0) {
|
|
622
|
+
warn(`dsh-topics-memory:按 ${ttl} 天 TTL 删除了 ${dropped.length} 条 deprecated topic(${dropped.map((s) => `topics/${s}`).join('、')});git 历史可找回`);
|
|
623
|
+
}
|
|
624
|
+
})
|
|
625
|
+
.catch(() => undefined);
|
|
626
|
+
}
|
|
589
627
|
});
|
|
590
628
|
}
|
|
591
629
|
}));
|
|
@@ -684,7 +722,22 @@ export function apply(ctx) {
|
|
|
684
722
|
}
|
|
685
723
|
return run.finally(() => releaseSessionLlm(sessionId));
|
|
686
724
|
};
|
|
687
|
-
|
|
725
|
+
// Manual /topics consolidate trigger: same lane as the cadence path, same
|
|
726
|
+
// trigger-time llm capture and in-flight guard (the command runs inside a
|
|
727
|
+
// live session, so its agent's scoped instance is the freshest adapter
|
|
728
|
+
// holder). Unlike the cadence path there is no releaseSessionLlm hook —
|
|
729
|
+
// the consolidator holds no per-session capture of its own.
|
|
730
|
+
const manualConsolidate = async (invocation) => {
|
|
731
|
+
try {
|
|
732
|
+
captureFromAgent(invocation.agent);
|
|
733
|
+
}
|
|
734
|
+
catch {
|
|
735
|
+
// contained — the candidate chain still walks whatever was captured
|
|
736
|
+
}
|
|
737
|
+
const sessionId = String(invocation.agent?.id ?? 'manual');
|
|
738
|
+
return consolidator.run(sessionId);
|
|
739
|
+
};
|
|
740
|
+
cmdCtx.effect(() => commands.register(buildTopicsCommand(service, mutate, resolveAsk, resolveLlm, manualDistill, manualConsolidate)), 'topics: /topics');
|
|
688
741
|
});
|
|
689
742
|
}
|
|
690
743
|
/** Config defaults for harnesses that skip schemastery's default application. */
|
|
@@ -711,5 +764,8 @@ const DEFAULTS = {
|
|
|
711
764
|
distillModel: '',
|
|
712
765
|
distillBatchSize: 40,
|
|
713
766
|
distillMaxModelCalls: 8,
|
|
767
|
+
consolidateCadence: 'daily',
|
|
768
|
+
deprecatedTtlDays: 15,
|
|
769
|
+
usageBoost: 0.15,
|
|
714
770
|
pushDebounceSeconds: 45,
|
|
715
771
|
};
|
package/lib/retrieval.d.ts
CHANGED
|
@@ -36,7 +36,22 @@ export interface RetrievalConfig {
|
|
|
36
36
|
* self-cross. Off for the explicit topic_search tool (the model asked).
|
|
37
37
|
*/
|
|
38
38
|
structuralGate: boolean;
|
|
39
|
+
/**
|
|
40
|
+
* UsageBoost (ADR 0015): behavioral bonus for topics used within the
|
|
41
|
+
* rolling window. Gate-scoped but double-locked — capped below the
|
|
42
|
+
* threshold and never granted to a zero-lexical candidate; the structural
|
|
43
|
+
* gate still requires lexical evidence. 0 = off.
|
|
44
|
+
*/
|
|
45
|
+
usageBoost: number;
|
|
46
|
+
/** Usage signals per slug (hits/opens in the window); absent map = no boost. */
|
|
47
|
+
usage?: ReadonlyMap<string, {
|
|
48
|
+
hits: number;
|
|
49
|
+
opens: number;
|
|
50
|
+
}>;
|
|
39
51
|
}
|
|
52
|
+
/** Hard cap for the usage bonus — below threshold 0.3 so usage can never
|
|
53
|
+
* carry a candidate across the gate on its own (ADR 0015). */
|
|
54
|
+
export declare const USAGE_BOOST_CAP = 0.2;
|
|
40
55
|
export declare const DEFAULT_RETRIEVAL: RetrievalConfig;
|
|
41
56
|
/** Trigger hits outrank every other lexical field (pi-llm-wiki: highest weight). */
|
|
42
57
|
export declare const TRIGGER_WEIGHT = 5;
|
|
@@ -70,7 +85,8 @@ export interface ScoredTopic {
|
|
|
70
85
|
strong: boolean;
|
|
71
86
|
/** Distinct query terms found in description ∪ conclusion — pass condition B is ≥2. */
|
|
72
87
|
bodyHits: number;
|
|
73
|
-
/** Score with the recency tiebreaker removed — the
|
|
88
|
+
/** Score with the recency tiebreaker removed — the score the numeric gate
|
|
89
|
+
* looks at (lexical + tags + usageBoost; recency excluded, ADR 0015). */
|
|
74
90
|
gateScore: number;
|
|
75
91
|
}
|
|
76
92
|
/** Score one topic against the query tokens. */
|
package/lib/retrieval.js
CHANGED
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
* @module retrieval
|
|
13
13
|
*/
|
|
14
14
|
import { dependsSlugs } from "./okf.js";
|
|
15
|
+
/** Hard cap for the usage bonus — below threshold 0.3 so usage can never
|
|
16
|
+
* carry a candidate across the gate on its own (ADR 0015). */
|
|
17
|
+
export const USAGE_BOOST_CAP = 0.2;
|
|
15
18
|
export const DEFAULT_RETRIEVAL = {
|
|
16
19
|
threshold: 0.3,
|
|
17
20
|
topK: 4,
|
|
@@ -19,6 +22,7 @@ export const DEFAULT_RETRIEVAL = {
|
|
|
19
22
|
graphDepth: 2,
|
|
20
23
|
recencyWindowDays: 7,
|
|
21
24
|
structuralGate: true,
|
|
25
|
+
usageBoost: 0.15,
|
|
22
26
|
};
|
|
23
27
|
/** Trigger hits outrank every other lexical field (pi-llm-wiki: highest weight). */
|
|
24
28
|
export const TRIGGER_WEIGHT = 5;
|
|
@@ -115,6 +119,18 @@ export function scoreTopic(queryTokens, topic, cfg) {
|
|
|
115
119
|
score += boost;
|
|
116
120
|
reasons.push(`tag-boost:+${boost.toFixed(2)}`);
|
|
117
121
|
}
|
|
122
|
+
// UsageBoost (ADR 0015) — gate-scoped behavioral bonus, unlike recency it
|
|
123
|
+
// IS part of gateScore. Double-locked: capped below the threshold, and a
|
|
124
|
+
// zero-lexical candidate (score === 0 here) never qualifies. Presence of a
|
|
125
|
+
// signal is the trigger, not its magnitude — votes never scale the bonus.
|
|
126
|
+
if (cfg.usageBoost > 0 && score > 0 && cfg.usage !== undefined) {
|
|
127
|
+
const u = cfg.usage.get(topic.slug);
|
|
128
|
+
if (u !== undefined && (u.hits > 0 || u.opens > 0)) {
|
|
129
|
+
const boost = Math.min(cfg.usageBoost, USAGE_BOOST_CAP);
|
|
130
|
+
score += boost;
|
|
131
|
+
reasons.push(`usage:+${boost.toFixed(2)}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
118
134
|
// Recency is a RANKING tiebreaker only (v4): it pads `score` but is kept
|
|
119
135
|
// out of `gateScore`, so it can never carry a candidate across the numeric
|
|
120
136
|
// threshold. Conflict demotion applies to the pre-tiebreaker score.
|
package/lib/service.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { type RetrievableTopic, type SearchOutcome } from './retrieval.ts';
|
|
|
10
10
|
import { type AssembleResult } from './digest.ts';
|
|
11
11
|
import type { BundleStore, Observation, SaveResult } from './store.ts';
|
|
12
12
|
import type { TopicsConfigValue } from './config.ts';
|
|
13
|
-
import { type AggregateStats, type QueryBuildShape } from './ilog.ts';
|
|
13
|
+
import { type AggregateStats, type QueryBuildShape, type UsageSignal } from './ilog.ts';
|
|
14
14
|
import type { Sync } from './sync.ts';
|
|
15
15
|
export interface RetrieveOutcome {
|
|
16
16
|
outcome: SearchOutcome;
|
|
@@ -38,6 +38,7 @@ export declare class TopicsService {
|
|
|
38
38
|
private cache;
|
|
39
39
|
/** mtime-keyed parse of the observations log, grouped by session. */
|
|
40
40
|
private echoCache;
|
|
41
|
+
private usageCache;
|
|
41
42
|
readonly store: BundleStore;
|
|
42
43
|
private readonly getConfig;
|
|
43
44
|
readonly sync?: Sync;
|
|
@@ -58,6 +59,14 @@ export declare class TopicsService {
|
|
|
58
59
|
* the injection hot path, mtime-cached like the roster.
|
|
59
60
|
*/
|
|
60
61
|
echoSlugsSync(sessionId: string): Set<string>;
|
|
62
|
+
/**
|
|
63
|
+
* Usage signals (ADR 0015): mtime-cached aggregate over the injection log
|
|
64
|
+
* and the opens log, rolling USAGE_WINDOW_DAYS. Synchronous like
|
|
65
|
+
* echoSlugsSync — the retrieval hot path is sync — and cheap when warm:
|
|
66
|
+
* a fresh cache costs two stat() calls. Unreadable logs fail open to an
|
|
67
|
+
* empty map (no boost), never an error.
|
|
68
|
+
*/
|
|
69
|
+
usageSignalsSync(): Map<string, UsageSignal>;
|
|
61
70
|
/**
|
|
62
71
|
* Full retrieval round: search → assemble → log (ADR 0006/0007). The
|
|
63
72
|
* injection record is written even for zero-hit rounds — the hit-rate
|
package/lib/service.js
CHANGED
|
@@ -11,12 +11,13 @@ import { join } from 'node:path';
|
|
|
11
11
|
import * as okf from "./okf.js";
|
|
12
12
|
import { searchTopics, scoreTopic, passesGate, tokenize } from "./retrieval.js";
|
|
13
13
|
import { assembleInjection, assemblePointer, POINTER_PER_TOPIC, POINTER_TOTAL } from "./digest.js";
|
|
14
|
-
import { aggregateStats, querySample } from "./ilog.js";
|
|
14
|
+
import { aggregateStats, querySample, aggregateUsage, USAGE_WINDOW_DAYS } from "./ilog.js";
|
|
15
15
|
import { fileHistory, fileAtRev } from "./git.js";
|
|
16
16
|
export class TopicsService {
|
|
17
17
|
cache = new Map();
|
|
18
18
|
/** mtime-keyed parse of the observations log, grouped by session. */
|
|
19
19
|
echoCache;
|
|
20
|
+
usageCache;
|
|
20
21
|
store;
|
|
21
22
|
getConfig;
|
|
22
23
|
sync;
|
|
@@ -66,6 +67,12 @@ export class TopicsService {
|
|
|
66
67
|
if (entry === undefined)
|
|
67
68
|
continue;
|
|
68
69
|
const doc = entry.doc;
|
|
70
|
+
// Deprecated topics are retired knowledge (consolidation merges and
|
|
71
|
+
// manual deprecations) — they must not rank, inject, or feed the
|
|
72
|
+
// depends-graph walk, or a merged-away entry would keep competing
|
|
73
|
+
// with its survivor for the injection budget.
|
|
74
|
+
if (doc.fm.status === 'deprecated')
|
|
75
|
+
continue;
|
|
69
76
|
out.push({
|
|
70
77
|
slug: entry.slug,
|
|
71
78
|
title: doc.fm.title,
|
|
@@ -85,6 +92,7 @@ export class TopicsService {
|
|
|
85
92
|
invalidate() {
|
|
86
93
|
this.cache.clear();
|
|
87
94
|
this.echoCache = undefined;
|
|
95
|
+
this.usageCache = undefined;
|
|
88
96
|
}
|
|
89
97
|
/**
|
|
90
98
|
* Slugs distilled from THIS session's own turns — the echo set (2026-09
|
|
@@ -139,6 +147,64 @@ export class TopicsService {
|
|
|
139
147
|
}
|
|
140
148
|
return bySession.bySession.get(sessionId) ?? new Set();
|
|
141
149
|
}
|
|
150
|
+
/**
|
|
151
|
+
* Usage signals (ADR 0015): mtime-cached aggregate over the injection log
|
|
152
|
+
* and the opens log, rolling USAGE_WINDOW_DAYS. Synchronous like
|
|
153
|
+
* echoSlugsSync — the retrieval hot path is sync — and cheap when warm:
|
|
154
|
+
* a fresh cache costs two stat() calls. Unreadable logs fail open to an
|
|
155
|
+
* empty map (no boost), never an error.
|
|
156
|
+
*/
|
|
157
|
+
usageSignalsSync() {
|
|
158
|
+
const injFile = join(this.store.metaDir(), 'injections.jsonl');
|
|
159
|
+
const opensFile = join(this.store.metaDir(), 'opens.jsonl');
|
|
160
|
+
const statOf = (file) => {
|
|
161
|
+
try {
|
|
162
|
+
const st = statSync(file);
|
|
163
|
+
return { mtimeMs: st.mtimeMs, size: st.size };
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return { mtimeMs: 0, size: 0 };
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const injSt = statOf(injFile);
|
|
170
|
+
const opensSt = statOf(opensFile);
|
|
171
|
+
const cached = this.usageCache;
|
|
172
|
+
if (cached !== undefined &&
|
|
173
|
+
cached.injMs === injSt.mtimeMs &&
|
|
174
|
+
cached.injSize === injSt.size &&
|
|
175
|
+
cached.opensMs === opensSt.mtimeMs &&
|
|
176
|
+
cached.opensSize === opensSt.size) {
|
|
177
|
+
return cached.signals;
|
|
178
|
+
}
|
|
179
|
+
const parseLines = (file) => {
|
|
180
|
+
const out = [];
|
|
181
|
+
try {
|
|
182
|
+
for (const line of readFileSync(file, 'utf8').split('\n')) {
|
|
183
|
+
if (line.trim() === '')
|
|
184
|
+
continue;
|
|
185
|
+
try {
|
|
186
|
+
out.push(JSON.parse(line));
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
continue; // torn tail line
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// unreadable log: contribute nothing
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
};
|
|
198
|
+
const signals = aggregateUsage(parseLines(injFile), parseLines(opensFile), USAGE_WINDOW_DAYS, Date.now());
|
|
199
|
+
this.usageCache = {
|
|
200
|
+
injMs: injSt.mtimeMs,
|
|
201
|
+
injSize: injSt.size,
|
|
202
|
+
opensMs: opensSt.mtimeMs,
|
|
203
|
+
opensSize: opensSt.size,
|
|
204
|
+
signals,
|
|
205
|
+
};
|
|
206
|
+
return signals;
|
|
207
|
+
}
|
|
142
208
|
/**
|
|
143
209
|
* Full retrieval round: search → assemble → log (ADR 0006/0007). The
|
|
144
210
|
* injection record is written even for zero-hit rounds — the hit-rate
|
|
@@ -162,6 +228,8 @@ export class TopicsService {
|
|
|
162
228
|
graphDepth: cfg.graphDepth,
|
|
163
229
|
recencyWindowDays: cfg.recencyWindowDays,
|
|
164
230
|
conflicts,
|
|
231
|
+
usageBoost: cfg.usageBoost,
|
|
232
|
+
usage: this.usageSignalsSync(),
|
|
165
233
|
});
|
|
166
234
|
const bySlug = new Map(roster.map((r) => [r.slug, r]));
|
|
167
235
|
const entries = [];
|
|
@@ -264,6 +332,8 @@ export class TopicsService {
|
|
|
264
332
|
graphDepth: cfg.graphDepth,
|
|
265
333
|
recencyWindowDays: cfg.recencyWindowDays,
|
|
266
334
|
conflicts,
|
|
335
|
+
usageBoost: cfg.usageBoost,
|
|
336
|
+
usage: this.usageSignalsSync(),
|
|
267
337
|
});
|
|
268
338
|
const exclude = dedup?.exclude;
|
|
269
339
|
const echo = dedup?.echo;
|
|
@@ -325,6 +395,8 @@ export class TopicsService {
|
|
|
325
395
|
recencyWindowDays: cfg.recencyWindowDays,
|
|
326
396
|
conflicts,
|
|
327
397
|
structuralGate: true,
|
|
398
|
+
usageBoost: cfg.usageBoost,
|
|
399
|
+
usage: this.usageSignalsSync(),
|
|
328
400
|
});
|
|
329
401
|
shadow.push({
|
|
330
402
|
slug: item.slug,
|
|
@@ -488,6 +560,9 @@ export class TopicsService {
|
|
|
488
560
|
if (entry === undefined)
|
|
489
561
|
continue;
|
|
490
562
|
const doc = entry.doc;
|
|
563
|
+
// Same retirement rule as roster(): deprecated knowledge never injects.
|
|
564
|
+
if (doc.fm.status === 'deprecated')
|
|
565
|
+
continue;
|
|
491
566
|
out.push({
|
|
492
567
|
slug: entry.slug,
|
|
493
568
|
title: doc.fm.title,
|
|
@@ -579,6 +654,8 @@ export class TopicsService {
|
|
|
579
654
|
// structural gate must not hide results.
|
|
580
655
|
structuralGate: false,
|
|
581
656
|
conflicts: await this.store.getConflicts(),
|
|
657
|
+
usageBoost: cfg.usageBoost,
|
|
658
|
+
usage: this.usageSignalsSync(),
|
|
582
659
|
});
|
|
583
660
|
}
|
|
584
661
|
async history(slug, limit = 20) {
|
package/lib/store.d.ts
CHANGED
|
@@ -66,6 +66,10 @@ export declare class BundleStore {
|
|
|
66
66
|
/** Last distill-lane run outcome (diagnostics for /topics status and e2e). */
|
|
67
67
|
writeDistillState(state: unknown): Promise<void>;
|
|
68
68
|
readDistillState(): Promise<Record<string, unknown> | undefined>;
|
|
69
|
+
private consolidateStatePath;
|
|
70
|
+
/** Last consolidation-lane run outcome (cadence stamp + /topics status). */
|
|
71
|
+
writeConsolidateState(state: unknown): Promise<void>;
|
|
72
|
+
readConsolidateState(): Promise<Record<string, unknown> | undefined>;
|
|
69
73
|
/** Create the directory skeleton, git repo, initial index, and repair
|
|
70
74
|
* wrapped depends entries left by older save paths (idempotent). */
|
|
71
75
|
ensure(): Promise<void>;
|
|
@@ -102,6 +106,13 @@ export declare class BundleStore {
|
|
|
102
106
|
actor?: string;
|
|
103
107
|
created?: boolean;
|
|
104
108
|
}): Promise<SaveResult>;
|
|
109
|
+
/**
|
|
110
|
+
* Remove a topic file and regenerate the index — one commit, so the rm is
|
|
111
|
+
* as traceable and revertible as every other bundle write. Callers own the
|
|
112
|
+
* policy (TTL housekeeping, future manual ops); the store only supplies the
|
|
113
|
+
* safe write path. Returns false when the slug doesn't exist (idempotent).
|
|
114
|
+
*/
|
|
115
|
+
deleteTopic(slug: string, message: string): Promise<boolean>;
|
|
105
116
|
private regenerateIndex;
|
|
106
117
|
/**
|
|
107
118
|
* Reverse-reference index: slug → who references it and how (`depends`
|
package/lib/store.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @module store
|
|
10
10
|
*/
|
|
11
|
-
import { readdir, readFile, writeFile, rename, mkdir, appendFile, access, } from 'node:fs/promises';
|
|
11
|
+
import { readdir, readFile, writeFile, rename, mkdir, appendFile, access, rm, } from 'node:fs/promises';
|
|
12
12
|
import { readFileSync } from 'node:fs';
|
|
13
13
|
import { constants as FsConstants } from 'node:fs';
|
|
14
14
|
import { dirname, join } from 'node:path';
|
|
@@ -93,6 +93,22 @@ export class BundleStore {
|
|
|
93
93
|
return undefined;
|
|
94
94
|
}
|
|
95
95
|
}
|
|
96
|
+
consolidateStatePath() {
|
|
97
|
+
return join(this.metaDir(), 'consolidate-state.json');
|
|
98
|
+
}
|
|
99
|
+
/** Last consolidation-lane run outcome (cadence stamp + /topics status). */
|
|
100
|
+
async writeConsolidateState(state) {
|
|
101
|
+
await mkdir(this.metaDir(), { recursive: true });
|
|
102
|
+
await atomicWrite(this.consolidateStatePath(), `${JSON.stringify(state, null, 2)}\n`);
|
|
103
|
+
}
|
|
104
|
+
async readConsolidateState() {
|
|
105
|
+
try {
|
|
106
|
+
return JSON.parse(await readFile(this.consolidateStatePath(), 'utf8'));
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
96
112
|
/** Create the directory skeleton, git repo, initial index, and repair
|
|
97
113
|
* wrapped depends entries left by older save paths (idempotent). */
|
|
98
114
|
async ensure() {
|
|
@@ -262,6 +278,24 @@ export class BundleStore {
|
|
|
262
278
|
return { slug, path: `topics/${slug}.md`, committed, created };
|
|
263
279
|
});
|
|
264
280
|
}
|
|
281
|
+
/**
|
|
282
|
+
* Remove a topic file and regenerate the index — one commit, so the rm is
|
|
283
|
+
* as traceable and revertible as every other bundle write. Callers own the
|
|
284
|
+
* policy (TTL housekeeping, future manual ops); the store only supplies the
|
|
285
|
+
* safe write path. Returns false when the slug doesn't exist (idempotent).
|
|
286
|
+
*/
|
|
287
|
+
async deleteTopic(slug, message) {
|
|
288
|
+
return this.enqueue(async () => {
|
|
289
|
+
const clean = okf.slugify(slug);
|
|
290
|
+
const file = this.topicPath(clean);
|
|
291
|
+
if (!(await existsSync(file)))
|
|
292
|
+
return false;
|
|
293
|
+
await rm(file);
|
|
294
|
+
await this.regenerateIndex();
|
|
295
|
+
await this.commit([`topics/${clean}.md`, 'index.md'], message);
|
|
296
|
+
return true;
|
|
297
|
+
});
|
|
298
|
+
}
|
|
265
299
|
async regenerateIndex() {
|
|
266
300
|
const metas = await this.listTopics();
|
|
267
301
|
const entries = metas.map((m) => ({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aiwayds/dsh-topics-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"description": "dsh-plugin: quality-first topic memory for dsh — a topic keeps the starting question, conclusion, impact and dependencies, not the process; local-first git-traced OKF bundle, budgeted LLM-free injection",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"build": "tsc -p tsconfig.json",
|
|
40
40
|
"check": "tsc --noEmit -p tsconfig.json",
|
|
41
41
|
"pretest": "tsc -p tsconfig.json",
|
|
42
|
-
"test": "node --test test/yaml.test.mjs test/okf.test.mjs test/retrieval.test.mjs test/digest.test.mjs test/ilog.test.mjs test/store.test.mjs test/paths.test.mjs test/git.test.mjs test/sync.test.mjs test/tools.test.mjs test/dedup.test.mjs test/commands.test.mjs test/onboard.test.mjs test/delegation.test.mjs test/observer.test.mjs test/distill.test.mjs test/viz.test.mjs test/corpus.test.mjs test/llm-scope.test.mjs test/settings-migration.test.mjs test/quality.test.mjs test/replay-gate.test.mjs test/depends-repair.test.mjs test/skill.test.mjs",
|
|
42
|
+
"test": "node --test test/yaml.test.mjs test/okf.test.mjs test/retrieval.test.mjs test/digest.test.mjs test/ilog.test.mjs test/store.test.mjs test/paths.test.mjs test/git.test.mjs test/sync.test.mjs test/tools.test.mjs test/dedup.test.mjs test/commands.test.mjs test/onboard.test.mjs test/delegation.test.mjs test/observer.test.mjs test/distill.test.mjs test/consolidate.test.mjs test/viz.test.mjs test/corpus.test.mjs test/llm-scope.test.mjs test/settings-migration.test.mjs test/quality.test.mjs test/replay-gate.test.mjs test/depends-repair.test.mjs test/skill.test.mjs",
|
|
43
43
|
"smoke": "node scripts/smoke-boot.mjs"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: dsh-topics-memory
|
|
3
|
-
description: "dsh 记忆插件(@aiwayds/dsh-topics-memory)使用与配置指南。凡涉及 dsh 记忆/话题库/GitHub
|
|
2
|
+
name: dsh-topics-memory-config
|
|
3
|
+
description: "dsh 记忆插件(@aiwayds/dsh-topics-memory)使用与配置指南。凡涉及 dsh 记忆/话题库/GitHub 同步/蒸馏/整理,或要配置 topics 段时先读本指南:settings.yaml 顶层 `topics:` 段全部键(repo/autoInject/topK/注入预算/蒸馏/整理/观察/图游走等)、/topics 命令族(onboard/status/distill/consolidate/stats/list/show/history/graph/sync/config/set)、首次配置 ask_user_question 向导(local-only 或绑 GitHub 仓、蒸馏模型路由、注入档位、自动观察)、注入形态 pointer/digest、legacy llmwiki 段自动迁移。触发词:topics、记忆、topic、蒸馏、distill、整理、consolidate、合并重复、deprecatedTtl、usageBoost、使用加成、autoInject、记忆库、llmwiki、include-subagents。"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# dsh-topics-memory 使用指南(工作记忆 / 蒸馏 / 注入)
|
|
@@ -36,6 +36,9 @@ description: "dsh 记忆插件(@aiwayds/dsh-topics-memory)使用与配置指
|
|
|
36
36
|
| `distillProvider` / `distillModel` | 空(蒸馏关闭) | 蒸馏模型路由,两者都非空才启用 |
|
|
37
37
|
| `distillBatchSize` | `40` | 每次蒸馏模型调用携带的观察条数(输出上限失败自动减半,下限 5) |
|
|
38
38
|
| `distillMaxModelCalls` | `8` | 单次蒸馏 run 的模型调用预算(预算耗尽即停,已成功批次保留标记) |
|
|
39
|
+
| `consolidateCadence` | `daily` | 整理 lane 节拍:daily/3d/7d/off。会话启动时检查上次整理时间,到期即在后台跑 LLM 园丁(复用蒸馏模型路由):合并重复、晋升 stable、废弃过时、刷新元数据;off 关闭 |
|
|
40
|
+
| `deprecatedTtlDays` | `15` | deprecated 条目超过 N 天在会话启动时自动删除(本地规则不依赖模型,逐条 git commit 可回溯找回);0 关闭清扫 |
|
|
41
|
+
| `usageBoost` | `0.15` | 使用加成(ADR 0015):近 30 天被注入命中/点开过的 Topic 检索加分(计入门槛分,帽 0.2,零词面相关不加,结构门不豁免);`0` 关闭 |
|
|
39
42
|
| `pushDebounceSeconds` | `45` | GitHub 模式去抖推送间隔 |
|
|
40
43
|
|
|
41
44
|
- 全部键都可用 `/topics set <键> <值>` 运行时写回同段(即时校验:boolean 用 on|off、
|
|
@@ -67,6 +70,7 @@ GitHub 模式凭据走 `$GITHUB_TOKEN` 或已登录的 gh CLI(`gh auth status`
|
|
|
67
70
|
| `/topics onboard` | 交互式配置向导(ask-user 面板,末步确认才写入) |
|
|
68
71
|
| `/topics status` | bundle 健康:topic 数、观察积压、冲突、最近蒸馏、同步状态 |
|
|
69
72
|
| `/topics distill` | 手动触发一次蒸馏 run(复用现有 lane 与 in-flight 守卫) |
|
|
73
|
+
| `/topics consolidate` | 手动触发一次整理 run:LLM 园丁合并重复/晋升/废弃/刷新元数据,逐条 git commit 可回滚 |
|
|
70
74
|
| `/topics stats` | 注入统计:hit rate、Top-N、near-miss 分布与调参建议 |
|
|
71
75
|
| `/topics list` | 表格列出全部 Topic(带序号、最新优先、100 条封顶) |
|
|
72
76
|
| `/topics show <slug>` | Topic 全文阅读页 + 反向引用(谁引用了我、怎么引用) |
|
|
@@ -83,9 +87,14 @@ GitHub 模式凭据走 `$GITHUB_TOKEN` 或已登录的 gh CLI(`gh auth status`
|
|
|
83
87
|
2. **蒸馏没跑**:distill-provider 与 distill-model 必须**同时非空**(缺一即闲置);长会话看
|
|
84
88
|
distill-every-turns 节拍、会话结束看 distill-on-session-end;`/topics distill` 手动触发
|
|
85
89
|
会给可读原因(no-model / in-flight / no-observations)。
|
|
86
|
-
3.
|
|
90
|
+
3. **整理 lane**:`consolidate-cadence`(daily/3d/7d,默认 daily,off 关闭)到期后在会话启动
|
|
91
|
+
时后台跑,复用蒸馏模型路由(未配蒸馏模型则闲置);本地词面聚类只把「长得像」的 topic
|
|
92
|
+
送模型,四类动作(merge/promote/deprecate/refresh)都逐条 git commit,`/topics consolidate`
|
|
93
|
+
手动立即跑,`/topics status` 看最近整理,不满意 `git revert` 即回滚。被合并条目标
|
|
94
|
+
deprecated 且不再参与注入。
|
|
95
|
+
4. **GitHub 同步**:repo 须形如 owner/name;推送有 push-debounce-seconds 去抖;退出只做
|
|
87
96
|
本地 commit 不等网络,推迟的 push 由下次启动 pull 后补推;rebase 冲突的 topic 降权标记,
|
|
88
97
|
`/topics status` 可查。
|
|
89
|
-
|
|
98
|
+
5. **旧版 llmwiki 段自动迁移**:0.5.x 的 `llmwiki:` 设置段与 `~/.dsh/llmwiki` 数据目录在
|
|
90
99
|
首次启动时自动迁移到 `topics` / `~/.dsh/topics`,无需手工;旧插件还在运行时会警告跳过
|
|
91
100
|
(防双载脑裂),先从 profile 移除旧包。
|