@aiwayds/dsh-topics-memory 0.12.0 → 0.13.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 CHANGED
@@ -103,15 +103,18 @@ First-time setup belongs to `/topics onboard`; day-to-day tuning is `/topics set
103
103
  | `repo` | empty (local-only) | GitHub sync repo `owner/name`; suggested `dsh-topics-data`; empty = back to local-only |
104
104
  | `autoInject` | `true` | Per-turn injection master switch |
105
105
  | `injectDedup` | `true` | Session-level injection dedup: topics already injected in this session are not re-injected (registry cleared at session end; budget-dropped topics stay injectable; deduped topK slots are NOT backfilled) — ADR 0012 |
106
+ | `suppressEcho` | `true` | Distill-echo suppression: topics distilled from the CURRENT session's own turns are not injected back into it (provenance rides the observations log `sessionId → distilledInto`) |
106
107
  | `topK` | `4` | Max topics injected per turn |
107
108
  | `perTopicBudget` | `300` | Per-topic digest token budget |
108
109
  | `totalBudget` | `1500` | Total injection budget per turn |
109
110
  | `matchThreshold` | `0.3` | Hit threshold; tune from `/topics stats` near-miss evidence |
110
- | `tagBoost` | `0.15` | Additive boost per tag hit |
111
+ | `tagBoost` | `0.15` | Additive boost per tag hit (total cap across hits equals this value) |
112
+ | `injectMode` | `pointer` | Injection shape: pointer (light pointers, ≤80 tok each, `topic_open` pulls the full text; total budget capped at 600) / digest (full digest rendering, per-topic 300 / total 1500) |
113
+ | `qualityLane` | `sampled` | Slow quality lane: `off` / `sampled` (1/3 of turns) / `always`; produced at `turn/end`, consumed by the next injection (consume-once), never for subagent sessions |
111
114
  | `graphDepth` | `2` | `depends` graph walk depth (0 disables) |
112
115
  | `recencyWindowDays` | `7` | Recency bonus window (+0.2) |
113
116
  | `autoObserve` | `true` | Capture atomic observations every turn |
114
- | `includeSubagents` | `true` | Whether injection and observation also engage subagent sessions (ADR 0011); `off` skips them entirely |
117
+ | `includeSubagents` | `false` | Whether injection and observation also engage subagent sessions (ADR 0011; off by default since 0.7.0); `off` skips them entirely |
115
118
  | `observationMaxChars` | `2000` | Per-side per-turn observation truncation |
116
119
  | `distillProvider` / `distillModel` | empty (distill off) | Distill lane model route; both must be set to enable. With a UI, `/topics set distill-provider` / `distill-model` without a value opens a picker panel (provider list → that provider's model catalog); a mixed `provider model` / `provider/model` value for `distill-model` splits into both keys |
117
120
  | `distillEveryTurns` | `5` | Distill every N turns of a long session |
@@ -132,7 +135,7 @@ This project's shape is directly inspired and supported by:
132
135
 
133
136
  ## Known boundaries
134
137
 
135
- - **Subagents engage memory by default — one switch to opt out**: by default (`include-subagents` on), injection and observation apply to subagent sessions too. `/topics set include-subagents off` skips delegated sessions entirely — no injection, no observation, no distill triggers; the topic tools stay on the global layer, so an explicit `topic_save` from a child still lands. Out-of-process subagents (claude-code/codex providers) never load this plugin anyway.
138
+ - **Subagents are out of memory by default — one switch to opt in**: by default (`include-subagents` off since 0.7.0) delegated sessions are skipped entirely — no injection, no observation, no distill triggers; `/topics set include-subagents on` applies injection and observation to them too. The topic tools stay on the global layer, so an explicit `topic_save` from a child still lands. Out-of-process subagents (claude-code/codex providers) never load this plugin anyway.
136
139
  - **Exit is local-only (0.10.0)**: the plugin's disposer makes one local git commit of the meta sidecars (observations / injections / distill state) and never waits on the network — no pull, no push, no model call, so host exit no longer pays a git round-trip or a bounded distill wait (the old 90s cap is gone; only a 10s guard against a pathological git stall remains). The exit distill trigger is fired fire-and-forget and is skipped entirely while a session-end run is still in flight (the same pool head would otherwise be fed to the model twice). Nothing is lost by skipping: observations are write-through on disk, the deferred push is replayed by the next boot's pull, and the skipped distill is replayed there too (boot-replay). `meta/distill-state.json` records each lane's outcome, checkable via `/topics status`.
137
140
  - **Observation GC (three strikes)**: an observation the model actually evaluated (parseable answer, however useless) but no op consumed accrues one failed attempt; the third failed attempt physically deletes it — explicitly authorized cleanup of raw data the lane demonstrably cannot process. Runs that never evaluated the batch never count: infrastructure failures (network errors, unconfigured distill route → readable `no-model` short-circuit) and unparseable output (`invalid-output`) are exempt, and a batch still mid-shrink on output-limit retries is only counted once a verdict is reached (success, floor stop, stall, or an explicit skip). Deletions are committed immediately (data destruction stays git-traceable); pure attempt counters follow the usual flush cadence.
138
141
  - **Config read timing**: `/topics set` and `settings.yaml` edits take effect most reliably from the next session start.
package/README.zh.md CHANGED
@@ -103,15 +103,18 @@ dsh plugin --profile <name> remove @aiwayds/dsh-topics-memory
103
103
  | `repo` | 空(local-only) | GitHub 同步仓 `owner/name`;建议 `dsh-topics-data`;置空回 local-only |
104
104
  | `autoInject` | `true` | 每轮注入总开关 |
105
105
  | `injectDedup` | `true` | 会话级注入去重:本会话已实际注入过的 Topic 不重注(会话结束清空注册表;被预算丢弃的仍可注入;被去重占用的 topK 名额不回填)——见 ADR 0012 |
106
+ | `suppressEcho` | `true` | 蒸馏回声抑制:本会话蒸馏出的 Topic 不回注同会话(provenance 记入 observations 日志的 `sessionId → distilledInto`) |
106
107
  | `topK` | `4` | 每轮最多注入的 Topic 数 |
107
108
  | `perTopicBudget` | `300` | 单 Topic 摘要 token 预算 |
108
109
  | `totalBudget` | `1500` | 每轮注入总预算 |
109
110
  | `matchThreshold` | `0.3` | 命中阈值;按 `/topics stats` 的 near-miss 证据调 |
110
- | `tagBoost` | `0.15` | tag 命中加成 |
111
+ | `tagBoost` | `0.15` | tag 命中加成(多次命中的总上限也为此值) |
112
+ | `injectMode` | `pointer` | 注入形态:pointer(轻指针,单条 ≤80 tok,`topic_open` 拉全文;总预算上界锁 600)/digest(完整摘要渲染,per-topic 300 / 总 1500) |
113
+ | `qualityLane` | `sampled` | 慢道质量 lane:`off`/`sampled`(1/3 轮触发)/`always`;`turn/end` 产出、下一轮注入消费(消费即清),子代理会话写死不跑 |
111
114
  | `graphDepth` | `2` | `depends` 图双向游走深度(0 关闭) |
112
115
  | `recencyWindowDays` | `7` | 近因加分窗口(+0.2) |
113
116
  | `autoObserve` | `true` | 每轮自动抓原子观察 |
114
- | `includeSubagents` | `true` | 注入与观察是否作用于子代理会话(ADR 0011);`off` = 子代理整体跳过 |
117
+ | `includeSubagents` | `false` | 注入与观察是否作用于子代理会话(ADR 0011;0.7.0 起默认 off);`off` = 子代理整体跳过 |
115
118
  | `observationMaxChars` | `2000` | 每侧每轮观察截断长度 |
116
119
  | `distillProvider` / `distillModel` | 空(蒸馏关闭) | 蒸馏 lane 模型路由,两者都设置才启用。有 UI 时 `/topics set distill-provider` / `distill-model` 不带值会弹选择面板(provider 列表 → 该 provider 的模型目录);带值时 `distill-model` 支持 `provider model` 或 `provider/model` 混写自动拆成两个键 |
117
120
  | `distillEveryTurns` | `5` | 长 session 每 N 轮触发一次蒸馏 |
@@ -132,7 +135,7 @@ dsh plugin --profile <name> remove @aiwayds/dsh-topics-memory
132
135
 
133
136
  ## 已知边界
134
137
 
135
- - **子代理默认参与记忆,可整体关掉**:默认(`include-subagents` 开)注入与观察同样作用于子代理会话。`/topics set include-subagents off` 后,delegation depth > 0 的子代理会话被整体跳过——不注入、不观察、不触发蒸馏;topic 工具始终在全局层(子代理显式 `topic_save` 不受开关影响)。跨进程子代理(claude-code/codex 等 provider)本就不加载本插件。
138
+ - **子代理默认不参与记忆,可整体打开**:默认(`include-subagents` off,0.7.0 起)delegation depth > 0 的子代理会话被整体跳过——不注入、不观察、不触发蒸馏;`/topics set include-subagents on` 后注入与观察同样作用于子代理会话。topic 工具始终在全局层(子代理显式 `topic_save` 不受开关影响)。跨进程子代理(claude-code/codex 等 provider)本就不加载本插件。
136
139
  - **退出路径本地化(0.10.0)**:插件 disposer 只做一步本地 git commit(meta 侧车文件:observations / injections / distill state),不再等待任何网络——没有 pull、没有 push、没有模型调用,宿主退出不再支付 git 双程与有界蒸馏等待(旧的 90s 上限移除,仅保留 10s 兜底以防病理性 git 卡死)。退出蒸馏触发改为 fire-and-forget;已有 session-end run 在跑时整体跳过(否则同一全局队头批次会被双份喂给模型)。跳过不丢活:observations 本就 write-through 落盘,推迟的 push 由下次启动的 pull 补推,被跳过的蒸馏同样由下次启动回放(boot-replay)。`meta/distill-state.json` 记录每次 lane 的结局,`/topics status` 可查。
137
140
  - **观察 GC(三振删除)**:被模型实际评估(返回了可解析应答,无论内容有无价值)却未被任何 op 消费的观察记一次 failed attempt,连续 3 次即物理删除(用户已明确授权删除 lane 确实无法处理的原始观察)。模型从未评估过的批次永不计数:基础设施失败(网络错误、蒸馏路由未配置 → 可读 `no-model` 短路)与输出不可解析(`invalid-output`)豁免;输出上限减半重试中的批次只有到达裁决(成功 / 触底 / stalled / 明确跳过)才计一次。删除立即 commit(数据销毁 git 可追溯),纯计数沿用 flush 节奏。
138
141
  - **配置读取时机**:`/topics set` 与 settings.yaml 修改在下次会话启动后生效最稳。
package/lib/index.d.ts CHANGED
@@ -1,20 +1,3 @@
1
- /**
2
- * dsh-topics-memory — OKF topic memory for DeepSeek Harness.
3
- *
4
- * Wiring (per ADR 0001–0008):
5
- * - `topics` settings namespace (user-tunable via /topics set)
6
- * - static systemPrompt section teaching the topic tools (never volatile
7
- * content — the provider cache prefix stays byte-stable)
8
- * - same-turn injection: retrieval runs synchronously at inbox-claim time
9
- * (`agent/inbox/spliced` live event, which dispatches BEFORE prompt
10
- * assembly — the only seam early enough, per the dsh-llmwiki upstream's
11
- * validated recipe) and a systemPrompt.context() provider serves the assembled
12
- * digest for this turn
13
- * - model tools: topic_save / topic_search / topic_observe / topic_history
14
- * - `/topics` command via the shared dsh-commands registry (optional peer)
15
- * - observer (M2): turn capture + distill triggers over session events
16
- * - sync (ADR 0003): pull on session start, debounced write-through push
17
- */
18
1
  import type { Context } from '@deepseek-ai/cordis';
19
2
  export declare const name = "dsh-topics-memory";
20
3
  /**
@@ -31,7 +14,8 @@ export declare const EXIT_COMMIT_TIMEOUT_MS = 10000;
31
14
  * naturally instead of being kept alive purely by the bound.
32
15
  */
33
16
  export declare function settleBounded(p: Promise<unknown> | undefined, ms: number): Promise<void>;
34
- /** Services consumed at apply time; llm joins via guarded ctx.inject. */
17
+ /** Services consumed at apply time; llm joins via guarded ctx.inject, and the
18
+ * skills registry serves the bundled usage/config guide. */
35
19
  export declare const inject: string[];
36
20
  /**
37
21
  * Settings-provider surface the legacy-namespace migration needs. Structurally
@@ -69,5 +53,16 @@ interface SettingsNsLike {
69
53
  * fire-and-forgets it.
70
54
  */
71
55
  export declare function migrateLegacySettings(settings: SettingsNsLike, warn?: (message: string) => void): Promise<void> | undefined;
56
+ /**
57
+ * Strip a leading YAML frontmatter block (`---` / body / `---`) from a skill
58
+ * markdown file. `SkillDefinition.content` must be the instruction body after
59
+ * metadata removal — the same shape the filesystem provider serves — so the
60
+ * bundled SKILL.md, which keeps its frontmatter for the GitHub/manual install
61
+ * paths, has the block removed when served through {@link skillProvider.get}.
62
+ * Tolerant by design: input that does not open with a `---` line, or whose
63
+ * frontmatter block is never closed, is returned unchanged. Mirrors the
64
+ * delimiter semantics of the upstream skill-filesystem provider.
65
+ */
66
+ export declare function stripFrontmatter(raw: string): string;
72
67
  export declare function apply(ctx: Context): void;
73
68
  export {};
package/lib/index.js CHANGED
@@ -1,3 +1,27 @@
1
+ /**
2
+ * dsh-topics-memory — OKF topic memory for DeepSeek Harness.
3
+ *
4
+ * Wiring (per ADR 0001–0008):
5
+ * - `topics` settings namespace (user-tunable via /topics set)
6
+ * - static systemPrompt section teaching the topic tools (never volatile
7
+ * content — the provider cache prefix stays byte-stable)
8
+ * - same-turn injection: retrieval runs synchronously at inbox-claim time
9
+ * (`agent/inbox/spliced` live event, which dispatches BEFORE prompt
10
+ * assembly — the only seam early enough, per the dsh-llmwiki upstream's
11
+ * validated recipe) and a systemPrompt.context() provider serves the assembled
12
+ * digest for this turn
13
+ * - model tools: topic_save / topic_search / topic_observe / topic_history
14
+ * - `/topics` command via the shared dsh-commands registry (optional peer)
15
+ * - bundled usage/config skill served through ctx.skills.registerProvider
16
+ * - observer (M2): turn capture + distill triggers over session events
17
+ * - sync (ADR 0003): pull on session start, debounced write-through push
18
+ */
19
+ import { readFile } from 'node:fs/promises';
20
+ import { fileURLToPath } from 'node:url';
21
+ /** Mirrors dsh-skill's bundled-skill rank (a non-load-bearing ordering hint;
22
+ * the constant is hardcoded there too). Local copy — see the type-import
23
+ * note above for why dsh-skill is not loaded at runtime here. */
24
+ const BUNDLED_SKILL_RANK = 600;
1
25
  import * as paths from "./paths.js";
2
26
  import { BundleStore } from "./store.js";
3
27
  import { TopicsService } from "./service.js";
@@ -34,8 +58,9 @@ export function settleBounded(p, ms) {
34
58
  const settled = p.then(() => undefined, () => undefined);
35
59
  return Promise.race([settled, cap]).finally(() => clearTimeout(timer));
36
60
  }
37
- /** Services consumed at apply time; llm joins via guarded ctx.inject. */
38
- export const inject = ['systemPrompt', 'tools', 'settings', 'agents', 'llm'];
61
+ /** Services consumed at apply time; llm joins via guarded ctx.inject, and the
62
+ * skills registry serves the bundled usage/config guide. */
63
+ export const inject = ['systemPrompt', 'tools', 'settings', 'agents', 'llm', 'skills'];
39
64
  // dsh-settings 0.1.2-alpha.3 removed the runtime settingsNamespace() helper:
40
65
  // register() now brand-checks the namespace at the type level
41
66
  // (SettingsNamespaceInput) and validates the same lowercase-hyphenated
@@ -99,7 +124,76 @@ export function migrateLegacySettings(settings, warn) {
99
124
  return undefined;
100
125
  }
101
126
  }
127
+ // --- Bundled skill -----------------------------------------------------------
128
+ /** Provider name under `ctx.skills`; doubles as the skill name. */
129
+ const SKILL_PROVIDER_NAME = 'dsh-topics-memory';
130
+ /** 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
+ /** Resource base served with the skill so its relative links resolve. */
133
+ const SKILL_RESOURCE_BASE = {
134
+ kind: 'directory',
135
+ path: fileURLToPath(new URL('../skills/dsh-topics-memory/', import.meta.url)),
136
+ };
137
+ const SKILL_INVOCATION = { modelInvocable: true, userInvocable: true };
138
+ /** Routing description; must stay identical to the SKILL.md frontmatter (asserted in tests). */
139
+ const SKILL_DESCRIPTION = 'dsh 记忆插件(@aiwayds/dsh-topics-memory)使用与配置指南。凡涉及 dsh 记忆/话题库/GitHub 同步/蒸馏,或要配置 topics 段时先读本指南:settings.yaml 顶层 `topics:` 段全部键(repo/autoInject/topK/注入预算/蒸馏/观察/图游走等)、/topics 命令族(onboard/status/distill/stats/list/show/history/graph/sync/config/set)、首次配置 ask_user_question 向导(local-only 或绑 GitHub 仓、蒸馏模型路由、注入档位、自动观察)、注入形态 pointer/digest、legacy llmwiki 段自动迁移。触发词:topics、记忆、topic、蒸馏、distill、autoInject、记忆库、llmwiki、include-subagents。';
140
+ const SKILL_CANDIDATE = {
141
+ name: SKILL_PROVIDER_NAME,
142
+ description: SKILL_DESCRIPTION,
143
+ invocation: SKILL_INVOCATION,
144
+ provider: SKILL_PROVIDER_NAME,
145
+ source: 'bundled',
146
+ resourceBase: SKILL_RESOURCE_BASE,
147
+ rank: BUNDLED_SKILL_RANK,
148
+ locator: SKILL_BODY_URL,
149
+ };
150
+ const skillProvider = {
151
+ name: SKILL_PROVIDER_NAME,
152
+ list: () => Promise.resolve([SKILL_CANDIDATE]),
153
+ async get(_candidate) {
154
+ return {
155
+ name: SKILL_CANDIDATE.name,
156
+ description: SKILL_CANDIDATE.description,
157
+ invocation: SKILL_CANDIDATE.invocation,
158
+ provider: SKILL_CANDIDATE.provider,
159
+ source: SKILL_CANDIDATE.source,
160
+ resourceBase: SKILL_RESOURCE_BASE,
161
+ content: stripFrontmatter(await readFile(SKILL_BODY_URL, 'utf8')),
162
+ };
163
+ },
164
+ };
165
+ /**
166
+ * Strip a leading YAML frontmatter block (`---` / body / `---`) from a skill
167
+ * markdown file. `SkillDefinition.content` must be the instruction body after
168
+ * metadata removal — the same shape the filesystem provider serves — so the
169
+ * bundled SKILL.md, which keeps its frontmatter for the GitHub/manual install
170
+ * paths, has the block removed when served through {@link skillProvider.get}.
171
+ * Tolerant by design: input that does not open with a `---` line, or whose
172
+ * frontmatter block is never closed, is returned unchanged. Mirrors the
173
+ * delimiter semantics of the upstream skill-filesystem provider.
174
+ */
175
+ export function stripFrontmatter(raw) {
176
+ const firstLineEnd = raw.indexOf('\n');
177
+ if (firstLineEnd < 0 || raw.slice(0, firstLineEnd).replace(/\r$/, '') !== '---')
178
+ return raw;
179
+ let lineStart = firstLineEnd + 1;
180
+ while (lineStart <= raw.length) {
181
+ const nextNewline = raw.indexOf('\n', lineStart);
182
+ const lineEnd = nextNewline < 0 ? raw.length : nextNewline;
183
+ if (raw.slice(lineStart, lineEnd).replace(/\r$/, '') === '---') {
184
+ return raw.slice(nextNewline < 0 ? raw.length : nextNewline + 1).trim();
185
+ }
186
+ if (nextNewline < 0)
187
+ return raw;
188
+ lineStart = nextNewline + 1;
189
+ }
190
+ return raw;
191
+ }
102
192
  export function apply(ctx) {
193
+ // `inject = ['skills']` guarantees the service exists on every real host;
194
+ // register unconditionally so a missing service fails loud instead of
195
+ // silently dropping the bundled skill.
196
+ ctx.skills.registerProvider(() => skillProvider);
103
197
  // dsh-settings is part of every real host closure but is a runtime-optional
104
198
  // peer so bare test harnesses can still load this module.
105
199
  const settingsNs = ctx.settings;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwayds/dsh-topics-memory",
3
- "version": "0.12.0",
3
+ "version": "0.13.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",
@@ -32,13 +32,14 @@
32
32
  "files": [
33
33
  "lib",
34
34
  "cordis.patch.yml",
35
- "src"
35
+ "src",
36
+ "skills"
36
37
  ],
37
38
  "scripts": {
38
39
  "build": "tsc -p tsconfig.json",
39
40
  "check": "tsc --noEmit -p tsconfig.json",
40
41
  "pretest": "tsc -p tsconfig.json",
41
- "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",
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
43
  "smoke": "node scripts/smoke-boot.mjs"
43
44
  },
44
45
  "dependencies": {},
@@ -47,6 +48,7 @@
47
48
  "@deepseek-ai/dsh-tools": ">=0.1.2-rc.1",
48
49
  "@deepseek-ai/dsh-llm": ">=0.1.2-rc.1",
49
50
  "@deepseek-ai/dsh-settings": ">=0.1.2-rc.1",
51
+ "@deepseek-ai/dsh-skill": ">=0.1.2-rc.1",
50
52
  "@deepseek-ai/dsh-commands": ">=0.1.2-rc.1",
51
53
  "@deepseek-ai/dsh-util-values": ">=0.1.2-rc.1",
52
54
  "@deepseek-ai/schemastery": "^3.18.2"
@@ -64,6 +66,9 @@
64
66
  "@deepseek-ai/dsh-settings": {
65
67
  "optional": true
66
68
  },
69
+ "@deepseek-ai/dsh-skill": {
70
+ "optional": true
71
+ },
67
72
  "@deepseek-ai/dsh-commands": {
68
73
  "optional": true
69
74
  },
@@ -86,6 +91,7 @@
86
91
  "@deepseek-ai/dsh-scope": "0.1.2-rc.1",
87
92
  "@deepseek-ai/dsh-session": "0.1.2-rc.1",
88
93
  "@deepseek-ai/dsh-settings": "0.1.2-rc.1",
94
+ "@deepseek-ai/dsh-skill": "0.1.2-rc.1",
89
95
  "@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
90
96
  "@deepseek-ai/dsh-timeout": "0.1.2-rc.1",
91
97
  "@deepseek-ai/dsh-tools": "0.1.2-rc.1",
@@ -0,0 +1,91 @@
1
+ ---
2
+ name: dsh-topics-memory
3
+ description: "dsh 记忆插件(@aiwayds/dsh-topics-memory)使用与配置指南。凡涉及 dsh 记忆/话题库/GitHub 同步/蒸馏,或要配置 topics 段时先读本指南:settings.yaml 顶层 `topics:` 段全部键(repo/autoInject/topK/注入预算/蒸馏/观察/图游走等)、/topics 命令族(onboard/status/distill/stats/list/show/history/graph/sync/config/set)、首次配置 ask_user_question 向导(local-only 或绑 GitHub 仓、蒸馏模型路由、注入档位、自动观察)、注入形态 pointer/digest、legacy llmwiki 段自动迁移。触发词:topics、记忆、topic、蒸馏、distill、autoInject、记忆库、llmwiki、include-subagents。"
4
+ ---
5
+
6
+ # dsh-topics-memory 使用指南(工作记忆 / 蒸馏 / 注入)
7
+
8
+ > dsh 插件:把会话中的「问题/结论/影响/依赖」蒸馏成结构化 topic,按相关度每轮回注上下文,
9
+ > 可选同步 GitHub 私有仓跨机共享。记忆是编辑出来的不是攒出来的——一个 topic 只记四件事
10
+ > (起始问题、结论、影响、依赖),过程属于 session,结束就随它去。
11
+
12
+ ## 配置入口(settings.yaml 顶层 `topics:` 段)
13
+
14
+ 在 `~/.dsh/settings.yaml` 写顶层 `topics:` 段(只写非默认值即可):
15
+
16
+ | 键 | 默认 | 作用 |
17
+ |---|---|---|
18
+ | `repo` | 空(local-only) | GitHub 同步仓 `owner/name`;空 = local-only(数据在 `~/.dsh/topics`,`$DSH_TOPICS_HOME` 可覆盖);建议仓库名 `dsh-topics-data`(与插件源码仓区分) |
19
+ | `autoInject` | `true` | 每轮注入总开关 |
20
+ | `injectDedup` | `true` | 会话级注入去重:本会话已实际注入过的 topic 不重注 |
21
+ | `suppressEcho` | `true` | 蒸馏回声抑制:本会话蒸馏出的 topic 不回注同会话 |
22
+ | `topK` | `4` | 每轮最多注入条数 |
23
+ | `perTopicBudget` | `300` | 单 topic 摘要 token 预算(digest 形态用;pointer 形态单条 ≤80 tok) |
24
+ | `totalBudget` | `1500` | 每轮注入总预算(digest 形态用;pointer 形态上界锁 600,旋钮可下调) |
25
+ | `matchThreshold` | `0.3` | 命中阈值;按 `/topics stats` 的 near-miss 证据调 |
26
+ | `tagBoost` | `0.15` | tag 命中加成(多次命中的总上限也为此值) |
27
+ | `injectMode` | `pointer` | 注入形态:pointer(轻指针,单条 ≤80 tok,`topic_open` 拉全文)/digest(完整摘要 300/1500) |
28
+ | `qualityLane` | `sampled` | 慢道质量 lane:off/sampled(1/3 轮)/always |
29
+ | `graphDepth` | `2` | `depends` 图双向游走深度(0 关闭) |
30
+ | `recencyWindowDays` | `7` | 近因加分窗口(+0.2) |
31
+ | `autoObserve` | `true` | 每轮自动抓原子观察(`topic_observe` 之外的后台捕获) |
32
+ | `includeSubagents` | `false` | 注入与观察是否作用于子代理会话;off 时子代理整体跳过(慢道写死不跑) |
33
+ | `observationMaxChars` | `2000` | 每侧每轮观察截断长度 |
34
+ | `distillEveryTurns` | `5` | 长 session 每 N 轮触发一次蒸馏 |
35
+ | `distillOnSessionEnd` | `true` | session 结束时蒸馏一次 |
36
+ | `distillProvider` / `distillModel` | 空(蒸馏关闭) | 蒸馏模型路由,两者都非空才启用 |
37
+ | `distillBatchSize` | `40` | 每次蒸馏模型调用携带的观察条数(输出上限失败自动减半,下限 5) |
38
+ | `distillMaxModelCalls` | `8` | 单次蒸馏 run 的模型调用预算(预算耗尽即停,已成功批次保留标记) |
39
+ | `pushDebounceSeconds` | `45` | GitHub 模式去抖推送间隔 |
40
+
41
+ - 全部键都可用 `/topics set <键> <值>` 运行时写回同段(即时校验:boolean 用 on|off、
42
+ inject-mode 限 pointer|digest、quality-lane 限 off|sampled|always、repo 须形如 owner/name;
43
+ distill-model 支持 `provider model` 或 `provider/model` 混写,自动拆成两个键)。
44
+ `/topics config` 看当前生效值;配置修改在下次会话启动后生效最稳。
45
+
46
+ ## 交互式配置向导(ask_user_question)
47
+
48
+ 用户说「帮我配置记忆 / 配置 topics」时优先引导跑 `/topics onboard`——dsh 原生 ask-user
49
+ 面板逐题收集(每题展示当前值,末步确认面板列出全部写入项,确认才写入;无 ask UI 的环境
50
+ 自动退化为逐条输入向导):
51
+
52
+ 1. **存储模式**:local-only(推荐,零配置零凭据,数据只在 `~/.dsh/topics`)/
53
+ GitHub 同步(`owner/name` 私有仓,写穿 + 去抖推送,跨机共享;默认建议
54
+ `<gh登录名>/dsh-topics-data`)。
55
+ 2. **蒸馏模型**:跳过(先不启用,观察只积累,随时可开)/指定 provider → 该 provider 目录下的
56
+ model(两键都非空才启用;选完经路由预校验,provider 无活路由阻断重选)。
57
+ 3. **注入档位**:保守(topK 2 · 800 tok)/标准(topK 4 · 1.5k tok,默认)/放量(topK 6 · 2.5k tok)。
58
+ 4. **自动观察**:保持开(推荐,随手聊就被记录)/关闭(只手动 topic_save / topic_observe)。
59
+
60
+ GitHub 模式凭据走 `$GITHUB_TOKEN` 或已登录的 gh CLI(`gh auth status` 检查;登录本身
61
+ 不在插件职责内,缺凭据先解决这一步)。
62
+
63
+ ## 命令速查
64
+
65
+ | 命令 | 作用 |
66
+ |------|------|
67
+ | `/topics onboard` | 交互式配置向导(ask-user 面板,末步确认才写入) |
68
+ | `/topics status` | bundle 健康:topic 数、观察积压、冲突、最近蒸馏、同步状态 |
69
+ | `/topics distill` | 手动触发一次蒸馏 run(复用现有 lane 与 in-flight 守卫) |
70
+ | `/topics stats` | 注入统计:hit rate、Top-N、near-miss 分布与调参建议 |
71
+ | `/topics list` | 表格列出全部 Topic(带序号、最新优先、100 条封顶) |
72
+ | `/topics show <slug>` | Topic 全文阅读页 + 反向引用(谁引用了我、怎么引用) |
73
+ | `/topics history <slug>` | 结论变更史(git log 工具化,表格化输出) |
74
+ | `/topics graph` | 生成关系图网页并自动在浏览器打开 |
75
+ | `/topics sync [pull\|push]` | GitHub 模式手动同步(默认自动) |
76
+ | `/topics config` | 当前生效配置(表格化输出) |
77
+ | `/topics set <键> <值>` | 运行时写回配置(校验见上);distill-provider / distill-model 不带值弹选择面板 |
78
+
79
+ ## 排障
80
+
81
+ 1. **注入太多/太吵**:下调 top-K、抬高 match-threshold 或压 total-budget;先看
82
+ `/topics stats` 的 near-miss 分布再动手,调参看证据不拍脑袋。
83
+ 2. **蒸馏没跑**:distill-provider 与 distill-model 必须**同时非空**(缺一即闲置);长会话看
84
+ distill-every-turns 节拍、会话结束看 distill-on-session-end;`/topics distill` 手动触发
85
+ 会给可读原因(no-model / in-flight / no-observations)。
86
+ 3. **GitHub 同步**:repo 须形如 owner/name;推送有 push-debounce-seconds 去抖;退出只做
87
+ 本地 commit 不等网络,推迟的 push 由下次启动 pull 后补推;rebase 冲突的 topic 降权标记,
88
+ `/topics status` 可查。
89
+ 4. **旧版 llmwiki 段自动迁移**:0.5.x 的 `llmwiki:` 设置段与 `~/.dsh/llmwiki` 数据目录在
90
+ 首次启动时自动迁移到 `topics` / `~/.dsh/topics`,无需手工;旧插件还在运行时会警告跳过
91
+ (防双载脑裂),先从 profile 移除旧包。
package/src/index.ts CHANGED
@@ -12,10 +12,25 @@
12
12
  * digest for this turn
13
13
  * - model tools: topic_save / topic_search / topic_observe / topic_history
14
14
  * - `/topics` command via the shared dsh-commands registry (optional peer)
15
+ * - bundled usage/config skill served through ctx.skills.registerProvider
15
16
  * - observer (M2): turn capture + distill triggers over session events
16
17
  * - sync (ADR 0003): pull on session start, debounced write-through push
17
18
  */
19
+ import { readFile } from 'node:fs/promises'
20
+ import { fileURLToPath } from 'node:url'
18
21
  import type { Context } from '@deepseek-ai/cordis'
22
+ // Types only (erased at emit). The runtime import is deliberately avoided:
23
+ // the registry-published dsh-skill lib imports host-closure siblings
24
+ // (@deepseek-ai/dsh-scope, dsh-llm — peers of it, but absent from a plugin
25
+ // repo's own dependency graph), which dies under pnpm's isolated layout.
26
+ // The host injects the real service at runtime; these types only shape the
27
+ // provider object this plugin hands it.
28
+ import type { SkillCandidate, SkillDefinition, SkillProvider } from '@deepseek-ai/dsh-skill'
29
+
30
+ /** Mirrors dsh-skill's bundled-skill rank (a non-load-bearing ordering hint;
31
+ * the constant is hardcoded there too). Local copy — see the type-import
32
+ * note above for why dsh-skill is not loaded at runtime here. */
33
+ const BUNDLED_SKILL_RANK = 600
19
34
  // Type-only side-effect import: loads dsh-settings' `declare module
20
35
  // '@deepseek-ai/cordis'` augmentation, which is what puts `ctx.settings` on
21
36
  // the Context type. There is no runtime import — the host provides the
@@ -66,8 +81,9 @@ export function settleBounded(p: Promise<unknown> | undefined, ms: number): Prom
66
81
  return Promise.race([settled, cap]).finally(() => clearTimeout(timer))
67
82
  }
68
83
 
69
- /** Services consumed at apply time; llm joins via guarded ctx.inject. */
70
- export const inject = ['systemPrompt', 'tools', 'settings', 'agents', 'llm']
84
+ /** Services consumed at apply time; llm joins via guarded ctx.inject, and the
85
+ * skills registry serves the bundled usage/config guide. */
86
+ export const inject = ['systemPrompt', 'tools', 'settings', 'agents', 'llm', 'skills']
71
87
 
72
88
  // dsh-settings 0.1.2-alpha.3 removed the runtime settingsNamespace() helper:
73
89
  // register() now brand-checks the namespace at the type level
@@ -160,7 +176,83 @@ export function migrateLegacySettings(settings: SettingsNsLike, warn?: (message:
160
176
  }
161
177
  }
162
178
 
179
+ // --- Bundled skill -----------------------------------------------------------
180
+
181
+ /** Provider name under `ctx.skills`; doubles as the skill name. */
182
+ const SKILL_PROVIDER_NAME = 'dsh-topics-memory'
183
+
184
+ /** Packaged skill body; `../skills/` resolves to the package root from both lib/ and src/. */
185
+ const SKILL_BODY_URL = new URL('../skills/dsh-topics-memory/SKILL.md', import.meta.url)
186
+
187
+ /** Resource base served with the skill so its relative links resolve. */
188
+ const SKILL_RESOURCE_BASE = {
189
+ kind: 'directory',
190
+ path: fileURLToPath(new URL('../skills/dsh-topics-memory/', import.meta.url)),
191
+ } as const
192
+
193
+ const SKILL_INVOCATION = { modelInvocable: true, userInvocable: true } as const
194
+
195
+ /** Routing description; must stay identical to the SKILL.md frontmatter (asserted in tests). */
196
+ const SKILL_DESCRIPTION = 'dsh 记忆插件(@aiwayds/dsh-topics-memory)使用与配置指南。凡涉及 dsh 记忆/话题库/GitHub 同步/蒸馏,或要配置 topics 段时先读本指南:settings.yaml 顶层 `topics:` 段全部键(repo/autoInject/topK/注入预算/蒸馏/观察/图游走等)、/topics 命令族(onboard/status/distill/stats/list/show/history/graph/sync/config/set)、首次配置 ask_user_question 向导(local-only 或绑 GitHub 仓、蒸馏模型路由、注入档位、自动观察)、注入形态 pointer/digest、legacy llmwiki 段自动迁移。触发词:topics、记忆、topic、蒸馏、distill、autoInject、记忆库、llmwiki、include-subagents。'
197
+
198
+ const SKILL_CANDIDATE: SkillCandidate = {
199
+ name: SKILL_PROVIDER_NAME,
200
+ description: SKILL_DESCRIPTION,
201
+ invocation: SKILL_INVOCATION,
202
+ provider: SKILL_PROVIDER_NAME,
203
+ source: 'bundled',
204
+ resourceBase: SKILL_RESOURCE_BASE,
205
+ rank: BUNDLED_SKILL_RANK,
206
+ locator: SKILL_BODY_URL,
207
+ }
208
+
209
+ const skillProvider: SkillProvider = {
210
+ name: SKILL_PROVIDER_NAME,
211
+ list: () => Promise.resolve([SKILL_CANDIDATE]),
212
+ async get(_candidate): Promise<SkillDefinition> {
213
+ return {
214
+ name: SKILL_CANDIDATE.name,
215
+ description: SKILL_CANDIDATE.description,
216
+ invocation: SKILL_CANDIDATE.invocation,
217
+ provider: SKILL_CANDIDATE.provider,
218
+ source: SKILL_CANDIDATE.source,
219
+ resourceBase: SKILL_RESOURCE_BASE,
220
+ content: stripFrontmatter(await readFile(SKILL_BODY_URL, 'utf8')),
221
+ }
222
+ },
223
+ }
224
+
225
+ /**
226
+ * Strip a leading YAML frontmatter block (`---` / body / `---`) from a skill
227
+ * markdown file. `SkillDefinition.content` must be the instruction body after
228
+ * metadata removal — the same shape the filesystem provider serves — so the
229
+ * bundled SKILL.md, which keeps its frontmatter for the GitHub/manual install
230
+ * paths, has the block removed when served through {@link skillProvider.get}.
231
+ * Tolerant by design: input that does not open with a `---` line, or whose
232
+ * frontmatter block is never closed, is returned unchanged. Mirrors the
233
+ * delimiter semantics of the upstream skill-filesystem provider.
234
+ */
235
+ export function stripFrontmatter(raw: string): string {
236
+ const firstLineEnd = raw.indexOf('\n')
237
+ if (firstLineEnd < 0 || raw.slice(0, firstLineEnd).replace(/\r$/, '') !== '---') return raw
238
+ let lineStart = firstLineEnd + 1
239
+ while (lineStart <= raw.length) {
240
+ const nextNewline = raw.indexOf('\n', lineStart)
241
+ const lineEnd = nextNewline < 0 ? raw.length : nextNewline
242
+ if (raw.slice(lineStart, lineEnd).replace(/\r$/, '') === '---') {
243
+ return raw.slice(nextNewline < 0 ? raw.length : nextNewline + 1).trim()
244
+ }
245
+ if (nextNewline < 0) return raw
246
+ lineStart = nextNewline + 1
247
+ }
248
+ return raw
249
+ }
250
+
163
251
  export function apply(ctx: Context): void {
252
+ // `inject = ['skills']` guarantees the service exists on every real host;
253
+ // register unconditionally so a missing service fails loud instead of
254
+ // silently dropping the bundled skill.
255
+ ctx.skills.registerProvider(() => skillProvider)
164
256
  // dsh-settings is part of every real host closure but is a runtime-optional
165
257
  // peer so bare test harnesses can still load this module.
166
258
  const settingsNs = (ctx as unknown as { settings?: { register(n: unknown, s: unknown): { get(): unknown } } }).settings