@aiwayds/dsh-topics-memory 0.14.0 → 0.16.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
@@ -4,7 +4,7 @@ English | [中文](README.zh.md)
4
4
 
5
5
  A dsh plugin: maintains "working topic memory" as an [OKF (Open Knowledge Format v0.2)](https://github.com/GoogleCloudPlatform/open-knowledge-format) knowledge bundle, persisted in a local git repository (optionally synced to a private GitHub repo), with conclusions traceable through git history, sessions automatically observed and distilled into knowledge, and relevant topics injected to the model before every turn.
6
6
 
7
- > **Requires dsh >= 0.1.2-rc.1** — this plugin targets the dsh RC/stable line only (CI and releases resolve the newest of the `latest`/`next` dist-tags at runtime). **The alpha line is no longer supported.**
7
+ > **Requires dsh >= 0.1.5-rc.2** — this plugin targets the dsh RC/stable line only (CI and releases resolve the newest of the `latest`/`next` dist-tags at runtime). **The alpha line is no longer supported.**
8
8
 
9
9
  ## Demo
10
10
 
package/README.zh.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  一个 dsh 插件:把「工作 topic 记忆」维护成 [OKF 标准(Open Knowledge Format v0.2)](https://github.com/GoogleCloudPlatform/open-knowledge-format)的知识 bundle,持久化在本地 git 仓库(可选同步到 GitHub 私有仓库),利用 git 历史提供结论可追溯性,自动观察会话沉淀知识,并在每轮对话前向模型注入相关 Topic。
6
6
 
7
- > **要求 dsh >= 0.1.2-rc.1** — 本插件只跟随 dsh RC/stable 线(CI 与发版在运行时解析 latest/next 中更新的 dist-tag)。**不再支持 alpha 线。**
7
+ > **要求 dsh >= 0.1.5-rc.2** — 本插件只跟随 dsh RC/stable 线(CI 与发版在运行时解析 latest/next 中更新的 dist-tag)。**不再支持 alpha 线。**
8
8
 
9
9
  ## 演示
10
10
 
package/lib/commands.d.ts CHANGED
@@ -26,3 +26,13 @@ export type ManualConsolidate = (invocation: CommandInvocation) => Promise<Conso
26
26
  export declare const HELP: string;
27
27
  export declare function buildTopicsCommand(service: TopicsService, mutate: MutateFn, resolveAsk?: AskServiceResolver, resolveLlm?: LlmDirectoryResolver, distillNow?: ManualDistill, consolidateNow?: ManualConsolidate): CommandDefinition;
28
28
  export declare function tuningHint(stats: ReturnType<typeof aggregateStats>, threshold: number): string | undefined;
29
+ /**
30
+ * Render the near-miss histogram as a two-line horizontal sparkline: tick
31
+ * labels over a log-scaled (count+1) strip, so a live session's 200:1 count
32
+ * range stays legible. Zero-count buckets inside the observed range draw as
33
+ * '·' to keep the score axis honest. Returns [] for empty or unparsable input.
34
+ */
35
+ export declare function renderNearMissSparkline(buckets: {
36
+ bucket: string;
37
+ count: number;
38
+ }[]): string[];
package/lib/commands.js CHANGED
@@ -267,9 +267,7 @@ async function renderStats(service) {
267
267
  lines.push(`| \`${cell(t.slug)}\` | ${t.count} |`);
268
268
  }
269
269
  if (stats.nearMissHistogram.length > 0) {
270
- lines.push('', 'Near-miss 分布(低于阈值或被结构门挡下):', '', '| 分数段 | 次数 |', '| --- | --- |');
271
- for (const b of stats.nearMissHistogram)
272
- lines.push(`| ${cell(b.bucket)} | ${b.count} |`);
270
+ lines.push('', `Near-miss 分布(低于阈值 ${service.cfg.matchThreshold.toFixed(2)} 或被结构门挡下,对数刻度):`, '', '```', ...renderNearMissSparkline(stats.nearMissHistogram), '```');
273
271
  const hint = tuningHint(stats, service.cfg.matchThreshold);
274
272
  if (hint !== undefined)
275
273
  lines.push('', `💡 ${hint}`);
@@ -286,6 +284,59 @@ export function tuningHint(stats, threshold) {
286
284
  }
287
285
  return undefined;
288
286
  }
287
+ const SPARK_LEVELS = '▁▂▃▄▅▆▇█';
288
+ const SPARK_EMPTY = '·';
289
+ /** Columns per histogram bucket; 2 keeps a 25-bucket range at 50 cols so the
290
+ * TUI renders both lines without wrapping. */
291
+ const SPARK_COLS_PER_BUCKET = 2;
292
+ /** Axis tick every 0.30 score units. */
293
+ const SPARK_TICK_STEP = 0.3;
294
+ /** Bucket width of the histogram built by nmBucket() in ilog.ts. */
295
+ const SPARK_BUCKET_WIDTH = 0.05;
296
+ /**
297
+ * Render the near-miss histogram as a two-line horizontal sparkline: tick
298
+ * labels over a log-scaled (count+1) strip, so a live session's 200:1 count
299
+ * range stays legible. Zero-count buckets inside the observed range draw as
300
+ * '·' to keep the score axis honest. Returns [] for empty or unparsable input.
301
+ */
302
+ export function renderNearMissSparkline(buckets) {
303
+ const starts = buckets.map((b) => Number(b.bucket.split('–')[0]));
304
+ if (buckets.length === 0 || starts.some((s) => Number.isNaN(s)))
305
+ return [];
306
+ const first = Math.min(...starts);
307
+ const last = Math.max(...starts);
308
+ const n = Math.round((last - first) / SPARK_BUCKET_WIDTH) + 1;
309
+ const counts = new Array(n).fill(0);
310
+ for (let i = 0; i < buckets.length; i++) {
311
+ counts[Math.round((starts[i] - first) / SPARK_BUCKET_WIDTH)] += buckets[i].count;
312
+ }
313
+ const max = Math.max(...counts);
314
+ const denom = Math.log10(max + 1);
315
+ const strip = counts
316
+ .map((c) => (c === 0 || denom === 0 ? SPARK_EMPTY : SPARK_LEVELS[Math.round((Math.log10(c + 1) / denom) * (SPARK_LEVELS.length - 1))]))
317
+ .map((ch) => ch.repeat(SPARK_COLS_PER_BUCKET))
318
+ .join('');
319
+ const width = n * SPARK_COLS_PER_BUCKET;
320
+ const line = new Array(width).fill(' ');
321
+ const used = new Array(width).fill(false);
322
+ const put = (at, label) => {
323
+ if (at < 0 || at + label.length > width)
324
+ return;
325
+ if (used.slice(at, at + label.length).some(Boolean))
326
+ return;
327
+ for (let k = 0; k < label.length; k++) {
328
+ line[at + k] = label[k];
329
+ used[at + k] = true;
330
+ }
331
+ };
332
+ for (let i = 0;; i += Math.round(SPARK_TICK_STEP / SPARK_BUCKET_WIDTH)) {
333
+ if (i > 0 && i >= n - 2)
334
+ break;
335
+ put(i * SPARK_COLS_PER_BUCKET, (first + i * SPARK_BUCKET_WIDTH).toFixed(2));
336
+ }
337
+ put(width - 4, (first + n * SPARK_BUCKET_WIDTH).toFixed(2));
338
+ return [line.join('').replace(/ +$/, ''), strip];
339
+ }
289
340
  /** /topics list caps the table at this many newest topics. */
290
341
  const LIST_LIMIT = 100;
291
342
  async function renderList(service) {
package/lib/observer.js CHANGED
@@ -79,10 +79,12 @@ export class Observer {
79
79
  state.assistantText = '';
80
80
  return;
81
81
  }
82
- case 'assistant/chunk': {
83
- const chunk = data.chunk;
84
- if (chunk?.type === 'text-delta' && typeof chunk.text === 'string')
85
- state.assistantText += chunk.text;
82
+ case 'assistant/message': {
83
+ // dsh 0.1.5-rc.1: streaming `assistant/chunk` events are gone — the
84
+ // settlement carries the committed message. Accumulate each round's
85
+ // text; turn/end reads the whole turn's transcript. Failed attempts
86
+ // (`assistant/attempt`) never reached the user, so they are skipped.
87
+ state.assistantText += textOf(data);
86
88
  return;
87
89
  }
88
90
  case 'turn/end': {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwayds/dsh-topics-memory",
3
- "version": "0.14.0",
3
+ "version": "0.16.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",
@@ -45,12 +45,12 @@
45
45
  "dependencies": {},
46
46
  "peerDependencies": {
47
47
  "@deepseek-ai/cordis": "^4.0.2",
48
- "@deepseek-ai/dsh-tools": ">=0.1.2-rc.1",
49
- "@deepseek-ai/dsh-llm": ">=0.1.2-rc.1",
50
- "@deepseek-ai/dsh-settings": ">=0.1.2-rc.1",
51
- "@deepseek-ai/dsh-skill": ">=0.1.2-rc.1",
52
- "@deepseek-ai/dsh-commands": ">=0.1.2-rc.1",
53
- "@deepseek-ai/dsh-util-values": ">=0.1.2-rc.1",
48
+ "@deepseek-ai/dsh-tools": ">=0.1.5-rc.2",
49
+ "@deepseek-ai/dsh-llm": ">=0.1.5-rc.2",
50
+ "@deepseek-ai/dsh-settings": ">=0.1.5-rc.2",
51
+ "@deepseek-ai/dsh-skill": ">=0.1.5-rc.2",
52
+ "@deepseek-ai/dsh-commands": ">=0.1.5-rc.2",
53
+ "@deepseek-ai/dsh-util-values": ">=0.1.5-rc.2",
54
54
  "@deepseek-ai/schemastery": "^3.18.2"
55
55
  },
56
56
  "peerDependenciesMeta": {
@@ -81,22 +81,22 @@
81
81
  },
82
82
  "devDependencies": {
83
83
  "@deepseek-ai/cordis": "4.0.2",
84
- "@deepseek-ai/dsh-agent": "0.1.2-rc.1",
85
- "@deepseek-ai/dsh-attachment": "0.1.2-rc.1",
86
- "@deepseek-ai/dsh-brand": "0.1.2-rc.1",
87
- "@deepseek-ai/dsh-code-runtime": "0.1.2-rc.1",
88
- "@deepseek-ai/dsh-commands": "0.1.2-rc.1",
89
- "@deepseek-ai/dsh-invariants": "0.1.2-rc.1",
90
- "@deepseek-ai/dsh-llm": "0.1.2-rc.1",
91
- "@deepseek-ai/dsh-scope": "0.1.2-rc.1",
92
- "@deepseek-ai/dsh-session": "0.1.2-rc.1",
93
- "@deepseek-ai/dsh-settings": "0.1.2-rc.1",
94
- "@deepseek-ai/dsh-skill": "0.1.2-rc.1",
95
- "@deepseek-ai/dsh-system-prompt": "0.1.2-rc.1",
96
- "@deepseek-ai/dsh-timeout": "0.1.2-rc.1",
97
- "@deepseek-ai/dsh-tools": "0.1.2-rc.1",
98
- "@deepseek-ai/dsh-user-approval": "0.1.2-rc.1",
99
- "@deepseek-ai/dsh-util-values": "0.1.2-rc.1",
84
+ "@deepseek-ai/dsh-agent": "0.1.5-rc.2",
85
+ "@deepseek-ai/dsh-attachment": "0.1.5-rc.2",
86
+ "@deepseek-ai/dsh-brand": "0.1.5-rc.2",
87
+ "@deepseek-ai/dsh-code-runtime": "0.1.5-rc.2",
88
+ "@deepseek-ai/dsh-commands": "0.1.5-rc.2",
89
+ "@deepseek-ai/dsh-invariants": "0.1.5-rc.2",
90
+ "@deepseek-ai/dsh-llm": "0.1.5-rc.2",
91
+ "@deepseek-ai/dsh-scope": "0.1.5-rc.2",
92
+ "@deepseek-ai/dsh-session": "0.1.5-rc.2",
93
+ "@deepseek-ai/dsh-settings": "0.1.5-rc.2",
94
+ "@deepseek-ai/dsh-skill": "0.1.5-rc.2",
95
+ "@deepseek-ai/dsh-system-prompt": "0.1.5-rc.2",
96
+ "@deepseek-ai/dsh-timeout": "0.1.5-rc.2",
97
+ "@deepseek-ai/dsh-tools": "0.1.5-rc.2",
98
+ "@deepseek-ai/dsh-user-approval": "0.1.5-rc.2",
99
+ "@deepseek-ai/dsh-util-values": "0.1.5-rc.2",
100
100
  "@deepseek-ai/schemastery": "3.18.2",
101
101
  "@types/node": "^22.19.0",
102
102
  "typescript": "^5.9.2"
package/src/commands.ts CHANGED
@@ -306,8 +306,14 @@ async function renderStats(service: TopicsService): Promise<string> {
306
306
  for (const t of stats.topTopics.slice(0, 5)) lines.push(`| \`${cell(t.slug)}\` | ${t.count} |`)
307
307
  }
308
308
  if (stats.nearMissHistogram.length > 0) {
309
- lines.push('', 'Near-miss 分布(低于阈值或被结构门挡下):', '', '| 分数段 | 次数 |', '| --- | --- |')
310
- for (const b of stats.nearMissHistogram) lines.push(`| ${cell(b.bucket)} | ${b.count} |`)
309
+ lines.push(
310
+ '',
311
+ `Near-miss 分布(低于阈值 ${service.cfg.matchThreshold.toFixed(2)} 或被结构门挡下,对数刻度):`,
312
+ '',
313
+ '```',
314
+ ...renderNearMissSparkline(stats.nearMissHistogram),
315
+ '```',
316
+ )
311
317
  const hint = tuningHint(stats, service.cfg.matchThreshold)
312
318
  if (hint !== undefined) lines.push('', `💡 ${hint}`)
313
319
  }
@@ -325,6 +331,57 @@ export function tuningHint(stats: ReturnType<typeof aggregateStats>, threshold:
325
331
  return undefined
326
332
  }
327
333
 
334
+ const SPARK_LEVELS = '▁▂▃▄▅▆▇█'
335
+ const SPARK_EMPTY = '·'
336
+ /** Columns per histogram bucket; 2 keeps a 25-bucket range at 50 cols so the
337
+ * TUI renders both lines without wrapping. */
338
+ const SPARK_COLS_PER_BUCKET = 2
339
+ /** Axis tick every 0.30 score units. */
340
+ const SPARK_TICK_STEP = 0.3
341
+ /** Bucket width of the histogram built by nmBucket() in ilog.ts. */
342
+ const SPARK_BUCKET_WIDTH = 0.05
343
+
344
+ /**
345
+ * Render the near-miss histogram as a two-line horizontal sparkline: tick
346
+ * labels over a log-scaled (count+1) strip, so a live session's 200:1 count
347
+ * range stays legible. Zero-count buckets inside the observed range draw as
348
+ * '·' to keep the score axis honest. Returns [] for empty or unparsable input.
349
+ */
350
+ export function renderNearMissSparkline(buckets: { bucket: string; count: number }[]): string[] {
351
+ const starts = buckets.map((b) => Number(b.bucket.split('–')[0]))
352
+ if (buckets.length === 0 || starts.some((s) => Number.isNaN(s))) return []
353
+ const first = Math.min(...starts)
354
+ const last = Math.max(...starts)
355
+ const n = Math.round((last - first) / SPARK_BUCKET_WIDTH) + 1
356
+ const counts = new Array<number>(n).fill(0)
357
+ for (let i = 0; i < buckets.length; i++) {
358
+ counts[Math.round((starts[i] - first) / SPARK_BUCKET_WIDTH)] += buckets[i].count
359
+ }
360
+ const max = Math.max(...counts)
361
+ const denom = Math.log10(max + 1)
362
+ const strip = counts
363
+ .map((c) => (c === 0 || denom === 0 ? SPARK_EMPTY : SPARK_LEVELS[Math.round((Math.log10(c + 1) / denom) * (SPARK_LEVELS.length - 1))]))
364
+ .map((ch) => ch.repeat(SPARK_COLS_PER_BUCKET))
365
+ .join('')
366
+ const width = n * SPARK_COLS_PER_BUCKET
367
+ const line = new Array<string>(width).fill(' ')
368
+ const used = new Array<boolean>(width).fill(false)
369
+ const put = (at: number, label: string) => {
370
+ if (at < 0 || at + label.length > width) return
371
+ if (used.slice(at, at + label.length).some(Boolean)) return
372
+ for (let k = 0; k < label.length; k++) {
373
+ line[at + k] = label[k]
374
+ used[at + k] = true
375
+ }
376
+ }
377
+ for (let i = 0; ; i += Math.round(SPARK_TICK_STEP / SPARK_BUCKET_WIDTH)) {
378
+ if (i > 0 && i >= n - 2) break
379
+ put(i * SPARK_COLS_PER_BUCKET, (first + i * SPARK_BUCKET_WIDTH).toFixed(2))
380
+ }
381
+ put(width - 4, (first + n * SPARK_BUCKET_WIDTH).toFixed(2))
382
+ return [line.join('').replace(/ +$/, ''), strip]
383
+ }
384
+
328
385
  /** /topics list caps the table at this many newest topics. */
329
386
  const LIST_LIMIT = 100
330
387
 
package/src/observer.ts CHANGED
@@ -111,9 +111,12 @@ export class Observer {
111
111
  state.assistantText = ''
112
112
  return
113
113
  }
114
- case 'assistant/chunk': {
115
- const chunk = (data as { chunk?: { type?: string; text?: string } }).chunk
116
- if (chunk?.type === 'text-delta' && typeof chunk.text === 'string') state.assistantText += chunk.text
114
+ case 'assistant/message': {
115
+ // dsh 0.1.5-rc.1: streaming `assistant/chunk` events are gone — the
116
+ // settlement carries the committed message. Accumulate each round's
117
+ // text; turn/end reads the whole turn's transcript. Failed attempts
118
+ // (`assistant/attempt`) never reached the user, so they are skipped.
119
+ state.assistantText += textOf(data as UserMessageLike)
117
120
  return
118
121
  }
119
122
  case 'turn/end': {