dsh-tool-folder 0.2.0 → 0.2.1

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
@@ -149,6 +149,19 @@ were never unregistered, so nothing else changes.
149
149
 
150
150
  ## Changelog
151
151
 
152
+ ### v0.2.1 — heat decay + exact-name priority (2026-08-30)
153
+ 1. **Hot heat decay** (`hotWindowDays`, default 3): a tool is promoted to
154
+ always-visible only when its call count stays inside the sliding window —
155
+ the old code promoted forever, so any tool used 3 times was permanently
156
+ resident and the fold silently decayed to zero over time. Legacy
157
+ `feedback.json` number entries are read as out-of-window → their stale
158
+ heat expires naturally. Set `hotWindowDays: 0` to keep the old behavior.
159
+ 2. **Exact-name priority in `tools_search`**: exact tool-name match always
160
+ ranks first, a ≥4-char query that is a name substring is pulled ahead of
161
+ BM25/semantic hits (deterministic fallback chain). Results stay
162
+ deduplicated.
163
+ 3. 40 tests green (11 new: window decay / legacy compat / priority order).
164
+
152
165
  ### v0.2.0 — first npm release (2026-08-30)
153
166
  This is the first version published to npm. It bundles the v0.1.7 firewall
154
167
  scope fix (exec-only hard block) and the v0.1.8 semantic leg +
package/lib/index.js CHANGED
@@ -33,7 +33,7 @@ import path from "node:path";
33
33
  import z from "@deepseek-ai/schemastery";
34
34
  import { buildIndex, search } from "./bm25.js";
35
35
  import { compressTool, normalizeDescription, sanitizeLossless } from "./schema.js";
36
- import { selectionCoverage } from "./metrics.js";
36
+ import { selectionCoverage, isHotTool, prioritizeExact } from "./metrics.js";
37
37
  import { ChainGuard, verdict } from "./chainguard.js";
38
38
  import { matchCategory } from "./category.js";
39
39
  import { toonifyValue } from "./toonify.js";
@@ -59,6 +59,8 @@ export const Config = z.object({
59
59
  deny: z.array(z.string()).default([]).description("禁用工具:精确名或 prefix* 前缀,命中不注入+拒绝执行"),
60
60
  topK: z.number().min(0).max(50).default(6).description("BM25 动态检索每轮最多注入的工具数"),
61
61
  hotThreshold: z.number().min(1).default(3).description("被折叠工具调用几次后自动晋升为常驻"),
62
+ hotWindowDays: z.number().min(0).max(365).default(3)
63
+ .description("hot 热度的滑动窗口(天):窗口内达标才常驻,超窗自动衰减;0=永不过期"),
62
64
  catalogEnabled: z.boolean().default(true).description("折叠安全网:把被藏工具列表写进提示词,模型知道它们存在"),
63
65
  compressLevel: z.union(COMPRESS_LEVELS).default("off").description("L1 schema 压缩级别:off 不压缩 / light 裁描述 / standard 裁描述+参数说明 / aggressive 删可选参数"),
64
66
  compressEnabled: z.boolean().default(false).description("兼容开关:compressLevel=off 且此开=true 时按 standard 处理"),
@@ -84,6 +86,7 @@ const DEFAULTS = {
84
86
  deny: [], // never inject + refuse execution (exact name or "prefix*")
85
87
  topK: 6,
86
88
  hotThreshold: 3,
89
+ hotWindowDays: 3, // v0.2.1: sliding-window heat decay (0 = never expire)
87
90
  catalogEnabled: true, // safety net: folded tools stay discoverable by name
88
91
  compressEnabled: false, // deprecated compat: compressLevel "off" + true → "standard"
89
92
  compressLevel: "off", // "off" | "light" | "standard" | "aggressive"
@@ -403,7 +406,13 @@ export function apply(ctx, config) {
403
406
  const q = String(args.query || "");
404
407
  const lim = args.limit ?? 8;
405
408
  if (!index || !q) return { query: q, results: [], total: 0 };
406
- const bm25Hits = search(index, q, lim * 2).map((i) => ({ id: allTools[i].name }));
409
+ // v0.2.1: exact-name > name-substring > BM25 priority (deterministic
410
+ // fallback chain, borrowed from Hermes-style tool search).
411
+ const bm25Hits = prioritizeExact(
412
+ allTools,
413
+ q,
414
+ search(index, q, lim * 2).map((i) => ({ id: allTools[i].name })),
415
+ );
407
416
  let fused = bm25Hits;
408
417
  // Semantic leg (P0-1): RRF-fuse BM25 with local bge-m3 similarity.
409
418
  // Any failure (Ollama down / timeout / cache miss on error) degrades
@@ -519,10 +528,12 @@ export function apply(ctx, config) {
519
528
 
520
529
  const agentId = context?.agent?.id || "default";
521
530
  const coreNames = new Set(cfg.perAgent?.[agentId]?.core || cfg.core || []);
531
+ // v0.2.1: hot = sliding-window decay (hotWindowDays). Tools not called
532
+ // inside the window lose their historical heat — the old code promoted
533
+ // forever, which slowly nullified the fold.
522
534
  const hotNames = new Set(
523
- Object.entries(feedback.calls || {})
524
- .filter(([, c]) => c >= cfg.hotThreshold)
525
- .map(([n]) => n),
535
+ Object.keys(feedback.calls || {}).filter((n) =>
536
+ isHotTool(feedback.calls, n, cfg.hotThreshold, cfg.hotWindowDays * 86400000)),
526
537
  );
527
538
 
528
539
  const stamp = tools.map((t) => t.name).join("\u0001");
@@ -673,8 +684,14 @@ export function apply(ctx, config) {
673
684
  });
674
685
  }
675
686
 
676
- // Heat counting (L4 feedback).
677
- feedback.calls[name] = (feedback.calls[name] || 0) + 1;
687
+ // Heat counting (L4 feedback). v0.2.1: upgrade to { n, ts } so the
688
+ // hot gate can decay stale heat; legacy number entries are kept
689
+ // readable by isHotTool (ts=0 → outside any positive window).
690
+ const prev = feedback.calls[name];
691
+ feedback.calls[name] = {
692
+ n: (typeof prev === "number" ? prev : prev?.n || 0) + 1,
693
+ ts: Date.now(),
694
+ };
678
695
 
679
696
  // Firewall: record the call sequence and flag exfil chains.
680
697
  if (cfg.firewallEnabled !== false) {
package/lib/metrics.js CHANGED
@@ -59,3 +59,68 @@ export function selectionCoverage(visibleNames, calledNames) {
59
59
  uncovered,
60
60
  };
61
61
  }
62
+
63
+ /**
64
+ * Hot-promotion gate with sliding-window decay (v0.2.1).
65
+ * feedback.calls entries may be:
66
+ * - new shape { n, ts } — n calls, ts = last call epoch ms;
67
+ * - legacy shape (number) — pre-v0.2.1 counts with no timestamp.
68
+ * A tool is hot when n >= hotThreshold AND (windowMs <= 0 OR the last call
69
+ * is inside the window). Legacy numbers carry ts=0 → outside any positive
70
+ * window → not hot (a tool that hasn't been called since the upgrade
71
+ * naturally loses its historical heat — this is the decay the old code
72
+ * lacked). windowMs=0 keeps the legacy forever-hot behavior for opt-out.
73
+ * @param {object|null} calls - feedback.calls map.
74
+ * @param {string} name - tool name.
75
+ * @param {number} hotThreshold - min calls to be hot.
76
+ * @param {number} windowMs - decay window; <=0 disables time checks.
77
+ * @returns {boolean}
78
+ */
79
+ export function isHotTool(calls, name, hotThreshold, windowMs) {
80
+ if (!calls || typeof calls !== "object") return false;
81
+ const v = calls[name];
82
+ if (v == null) return false;
83
+ const n = typeof v === "number" ? v : v.n;
84
+ const ts = typeof v === "number" ? 0 : v.ts;
85
+ if (!(n >= hotThreshold)) return false;
86
+ if (windowMs > 0 && !(ts > 0 && Date.now() - ts <= windowMs)) return false;
87
+ return true;
88
+ }
89
+
90
+ /**
91
+ * Deterministic retrieval priority (v0.2.1, borrowed from Hermes-style
92
+ * "exact name > name token > description" fallback chains): the query's
93
+ * exact tool-name match always ranks first; a ≥4-char query that is a
94
+ * substring of a tool name is pulled ahead of BM25/semantic hits. Everything
95
+ * else keeps the given hit order, deduplicated.
96
+ * @param {Array<{name: string}>} docs - the tool corpus (allTools).
97
+ * @param {string} q - the search query.
98
+ * @param {Array<{id: string, score: number}>} hits - ranked hits.
99
+ * @returns {Array<{id: string, score: number}>} re-ordered hits.
100
+ */
101
+ export function prioritizeExact(docs, q, hits) {
102
+ const query = String(q || "").trim().toLowerCase();
103
+ if (!query || !Array.isArray(docs) || !Array.isArray(hits)) return hits;
104
+ const ordered = [];
105
+ const seen = new Set();
106
+ const exact = docs.find((d) => String(d.name || "").toLowerCase() === query);
107
+ if (exact) {
108
+ ordered.push({ id: exact.name, score: 0 });
109
+ seen.add(exact.name);
110
+ }
111
+ if (query.length >= 4) {
112
+ for (const d of docs) {
113
+ if (seen.has(d.name)) continue;
114
+ if (String(d.name || "").toLowerCase().includes(query)) {
115
+ ordered.push({ id: d.name, score: 0 });
116
+ seen.add(d.name);
117
+ }
118
+ }
119
+ }
120
+ for (const h of hits) {
121
+ if (!h || seen.has(h.id)) continue;
122
+ ordered.push(h);
123
+ seen.add(h.id);
124
+ }
125
+ return ordered;
126
+ }
package/package.json CHANGED
@@ -1,48 +1,48 @@
1
- {
2
- "name": "dsh-tool-folder",
3
- "version": "0.2.0",
4
- "description": "Fold the DSH tool surface per request + ChainGuard firewall (high-risk block + exfil-chain detection + anti-obfuscation) + BM25/bge-m3 hybrid tools_search. Shrinks schema tokens 80-90% while keeping selection accuracy. v0.2.0 adds a semantic retrieval leg (local Ollama bge-m3, RRF hybrid) and ChainGuard obfuscation detection (concat-rebuild / -enc / certutil / bitsadmin / homoglyphs).",
5
- "keywords": [
6
- "dsh-plugin",
7
- "dsh",
8
- "deepseek-harness",
9
- "cordis",
10
- "tool-selection",
11
- "token-optimization",
12
- "bm25",
13
- "semantic-search",
14
- "security",
15
- "firewall",
16
- "obfuscation"
17
- ],
18
- "type": "module",
19
- "main": "lib/index.js",
20
- "files": [
21
- "lib",
22
- "cordis.patch.yml",
23
- "README.md"
24
- ],
25
- "dsh": {
26
- "bundle": {
27
- "patch": "./cordis.patch.yml"
28
- }
29
- },
30
- "license": "MIT",
31
- "engines": {
32
- "node": ">=18"
33
- },
34
- "dependencies": {
35
- "@deepseek-ai/schemastery": "^3.18.1"
36
- },
37
- "peerDependencies": {
38
- "@deepseek-ai/cordis": "^4.0.1",
39
- "@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
40
- },
41
- "scripts": {
42
- "test": "node --test \"test/*.test.js\""
43
- },
44
- "exports": {
45
- ".": "./lib/index.js",
46
- "./lib/*": "./lib/*"
47
- }
48
- }
1
+ {
2
+ "name": "dsh-tool-folder",
3
+ "version": "0.2.1",
4
+ "description": "Fold the DSH tool surface per request + ChainGuard firewall (high-risk block + exfil-chain detection + anti-obfuscation) + BM25/bge-m3 hybrid tools_search. Shrinks schema tokens 80-90% while keeping selection accuracy. v0.2.0 adds a semantic retrieval leg (local Ollama bge-m3, RRF hybrid) and ChainGuard obfuscation detection (concat-rebuild / -enc / certutil / bitsadmin / homoglyphs).",
5
+ "keywords": [
6
+ "dsh-plugin",
7
+ "dsh",
8
+ "deepseek-harness",
9
+ "cordis",
10
+ "tool-selection",
11
+ "token-optimization",
12
+ "bm25",
13
+ "semantic-search",
14
+ "security",
15
+ "firewall",
16
+ "obfuscation"
17
+ ],
18
+ "type": "module",
19
+ "main": "lib/index.js",
20
+ "files": [
21
+ "lib",
22
+ "cordis.patch.yml",
23
+ "README.md"
24
+ ],
25
+ "dsh": {
26
+ "bundle": {
27
+ "patch": "./cordis.patch.yml"
28
+ }
29
+ },
30
+ "license": "MIT",
31
+ "engines": {
32
+ "node": "\u003e=18"
33
+ },
34
+ "dependencies": {
35
+ "@deepseek-ai/schemastery": "^3.18.1"
36
+ },
37
+ "peerDependencies": {
38
+ "@deepseek-ai/cordis": "^4.0.1",
39
+ "@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
40
+ },
41
+ "scripts": {
42
+ "test": "node --test \"test/*.test.js\""
43
+ },
44
+ "exports": {
45
+ ".": "./lib/index.js",
46
+ "./lib/*": "./lib/*"
47
+ }
48
+ }