dsh-context-toolkit 0.3.2 → 0.3.4

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/CHANGELOG.md ADDED
@@ -0,0 +1,48 @@
1
+ # Changelog (dsh-context-toolkit)
2
+
3
+ > [简体中文](./CHANGELOG.zh.md) | **English**
4
+
5
+ User-visible changes, newest first. Commit hashes in brackets.
6
+
7
+ ## 0.3.4
8
+ - **Fixed**: output that a tool already truncated is no longer compressed a second time. An upstream tool (`ops_bash`) truncates its own output and spills the rest to a file, and the sandbox (`ctk_execute`) caps a run at 256 KB and closes with a `~~~` line; this layer used to compress those already-handled bodies again — saving nothing, storing a second copy of the same original, and counting the same traffic twice. Both markers (`[output truncated; full output: …]` and a standalone `~~~` line) are recognised and skipped, and the reason is reported in a new "skip reasons" ledger line. [`TBD`]
9
+
10
+ ## 0.3.3
11
+ - **Docs**: adds this bilingual changelog, listed in the manifest and gated by release-check; the README and repository description were refreshed too. The npm package now carries the changelog with each release. [`01d7107`]
12
+
13
+ ## 0.3.2
14
+ - **Fixed**: `slimOriginalTtlMinutes` was displayed in the settings page and never read by the code, so the expiry the panel promised never happened and stored originals accumulated without limit; the sweep now runs once on every load.
15
+ - **Fixed**: the rollback call `purgeBlobs({ all: false })` passed no age and was therefore a no-op, leaving the original forever; a rollback now removes exactly the id it wrote and cannot take an unrelated original with it.
16
+ - The ledger reports the effective retention, making a consumed setting visible. [`98f6a06`]
17
+
18
+ ## 0.3.1
19
+ - **Fixed**: `ctk_retrieve` omitted `isError` on its success path; the host validates a tool's output against its schema and rejects the call when a required property is missing, so read-back was unusable — the marker was injected and the original stored, and it could not be read. [`75ea559`]
20
+ - The ledger now renders slimming counts (blocks, bytes, tokens, skipped) and stored originals, and counts slimmed bytes as kept out of context.
21
+
22
+ ## 0.3.0
23
+ - **New: output slimming at `tools/post-execute`** — the gate guards the door; this handles what is already inside: uniform JSON re-encoded losslessly, tables keeping header and both ends with the middle sampled by original line number, grep/search folded by file, logs folded with diagnostics kept verbatim, code and key-value left alone.
24
+ - **Two never-worse gates**: at most 72% of the bytes and a strictly lower token estimate, or the candidate is dropped.
25
+ - **Reversibility is the red line**: every lossy path stores the exact original first and injects a `[ctk_retrieve id=…]` marker, read back with the new `ctk_retrieve`. Originals live in the **same SQLite database as the FTS5 index** (a `blobs` table; BLOB access so nothing re-encodes the bytes). [`61f162d`]
26
+
27
+ ## 0.2.4
28
+ - **Fixed**: the `read` branch repeated `script_path`'s path-base mistake — a relative path was resolved against the plugin process's cwd while the host opens it against the session working directory; measured on live code, the same 70 KB file was refused by absolute path and read whole by relative path. [`8607fb2`]
29
+ - **New**: the host's own fetchers (`web_fetch`, `read_page`) are nudged too — previously curl in a shell was refused while the same page walked in through them.
30
+
31
+ ## 0.2.3
32
+ - **Fixed**: a relative `script_path` was resolved against a different base than the host uses, so the gate read nothing and waved the call through; resolution now uses the session working directory. [`5820210`]
33
+
34
+ ## 0.2.2
35
+ - **Fixed**: gate matching moved from a whole-line substring with delimiters to splitting at command separators and comparing each segment's command word — fixing both directions (misses on non-leading invocations and prefixed/pathed ones, false denials where the tool name is just an argument).
36
+ - **Fixed**: `ops_bash` runs a file when handed `script_path`, which the gate never saw; the script is now read (first 256 KB).
37
+ - **Fixed**: the suite leaked its own handles (the mock discarded the disposer, so the SQLite handle stayed open and Windows could not delete the temp directory). [`da1b0fd`]
38
+
39
+ ## 0.2.1
40
+ - **Fixed**: `ctk_doctor` printed a hardcoded `0.1.0` while the bundle shipped 0.2.0; it now reads its own `package.json` at load.
41
+ - **New**: `release-check.mjs`, the release-readiness gate. [`d98835a`]
42
+
43
+ ## 0.2.0
44
+ - **Fixed**: `guard` returned `null` instead of `undefined` when allowing, and the host denies whenever the reason is not undefined — every tool call was denied (run_code failed with a bare `Error: null`).
45
+ - **Fixed**: the gate recognised only the shell names it shipped with, so `curl` through `ops_bash` slipped past; every shell name is covered now.
46
+
47
+ ## 0.1.0
48
+ - First release: `ctk_execute` / `ctk_execute_file` / `ctk_batch_execute` (run code in a child process, return only the conclusion), `ctk_index` / `ctk_search` (a local FTS5 knowledge base on `node:sqlite`), the routing gate, `ctk_stats` / `ctk_doctor` and a live occupancy panel. [`87b35fe`]
@@ -0,0 +1,48 @@
1
+ # 更新说明(dsh-context-toolkit)
2
+
3
+ > **简体中文** | [English](./CHANGELOG.md)
4
+
5
+ 本文件按版本记录用户可感知的变化。方括号内为对应提交。
6
+
7
+ ## 0.3.4
8
+ - **修复**:对**已被上游截断**的输出不再重复压缩。上游工具(如 `ops_bash`)会自行截断并落盘,沙箱(`ctk_execute`)也有 256 KB 捕获上限并以 `~~~` 收尾;此前本层会对这些**已经处理过**的产物再压一次 —— 省不了任何字节,却把同一份原文**又存一遍**,等于把同一份流量记两次收益。现识别这两种截断标记(`[output truncated; full output: …]` 与独立的 `~~~` 行)并跳过,同时把跳过原因计入台账新增的「跳过原因」一行。[`TBD`]
9
+
10
+ ## 0.3.3
11
+ - **文档**:新增中英双语更新说明(本文件),并纳入发布清单与发布门;README 与仓库首页说明同步更新。npm 包开始随版本携带 CHANGELOG。[`01d7107`]
12
+
13
+ ## 0.3.2
14
+ - **修复**:`slimOriginalTtlMinutes`(原文保留期)此前只在设置页展示、代码从未读取 ⇒ 面板承诺的过期从不发生,原文无限累积;现于**每次加载**执行一次过期清理。
15
+ - **修复**:标记投递失败时的回滚调用 `purgeBlobs({ all: false })` 不传年龄 ⇒ **是空操作**,原文永久留存;现按**精确 id** 回滚,不会牵连其他原文。
16
+ - 台账新增「原文保留期 retention」一行,让被消费的配置可见。[`98f6a06`]
17
+
18
+ ## 0.3.1
19
+ - **修复**:`ctk_retrieve` 成功路径漏 `isError` ⇒ 宿主按 schema 校验工具输出、缺必需属性直接拒调用,**可逆取回完全不可用**(标记已注入、原文已入库,却取不回来);现补上。[`75ea559`]
20
+ - 台账渲染「输出瘦身 slimmed(块/字节/token/跳过数)」与「可逆原文 blobs」,并把瘦身字节计入 *kept out of context*。
21
+
22
+ ## 0.3.0
23
+ - **新增:出口压缩(`tools/post-execute`)**——门禁守入口,这里处理**已经进来**的输出:同构 JSON **无损**表化重编码、表格保留表头+首尾并按**原始行号**采样中段、grep/search 按文件折叠、日志折叠普通行而**逐字保留诊断行**、代码与键值对**不压**。
24
+ - **双重门**:字节 ≤ 72% 且估算 token 严格下降,压不动就不压。
25
+ - **可逆红线**:有损路径先把原文**逐字节**存好并注入 `[ctk_retrieve id=…]` 标记;新增 `ctk_retrieve` 取回。原文与 FTS5 索引同在**同一个 SQLite 库**(`blobs` 表,BLOB 存取避免重编码)。[`61f162d`]
26
+
27
+ ## 0.2.4
28
+ - **修复**:`read` 分支重犯了 `script_path` 的路径基准错——相对路径按插件进程 cwd 解析,而宿主按**会话工作目录**打开;实测同一份 70000 字节文件绝对路径被拦、相对路径整段进对话。[`8607fb2`]
29
+ - **新增**:把宿主自带取网页工具(`web_fetch` / `read_page`)纳入整页软引导(此前 shell 里的 curl 被拦而同页可从它们走进来)。
30
+
31
+ ## 0.2.3
32
+ - **修复**:相对 `script_path` 解析基准与宿主不一致 ⇒ 读不到文件即当作「无脚本」放行;现统一按会话 cwd 解析。[`5820210`]
33
+
34
+ ## 0.2.2
35
+ - **修复**:门禁匹配从「整行子串 + 两侧分隔符」改为**按命令分隔符分段、只比较各段命令词**——修掉双向错误(漏检非首位调用与带前缀/路径的调用;误拦工具名只是参数的文本)。
36
+ - **修复**:`ops_bash` 用 `script_path` 时载荷在文件里、门禁完全看不见 ⇒ 补读脚本前 256 KB。
37
+ - **修复**:套件自身的句柄泄漏(mock 丢弃 disposer ⇒ SQLite 句柄常开、Windows 删不掉临时目录)。[`da1b0fd`]
38
+
39
+ ## 0.2.1
40
+ - **修复**:`ctk_doctor` 版本号硬编码 `0.1.0` 而包已 0.2.0 ⇒ 改为启动时读自身 `package.json`。
41
+ - **新增**:`release-check.mjs`(发布就绪门)。[`d98835a`]
42
+
43
+ ## 0.2.0
44
+ - **修复**:`guard` 放行时返回 `null` 而非 `undefined` ⇒ 宿主按 `reason !== undefined` 判拒,**每个工具调用都被拒**(run_code 报裸 `Error: null`)。
45
+ - **修复**:门禁只认识自带的 shell 名 ⇒ `ops_bash` 上同样的 curl 可绕过;现覆盖全部 shell 名。
46
+
47
+ ## 0.1.0
48
+ - 首个版本:`ctk_execute` / `ctk_execute_file` / `ctk_batch_execute`(子进程跑码只回结论)、`ctk_index` / `ctk_search`(基于 `node:sqlite` 的本地 FTS5 知识库)、路由门禁、`ctk_stats` / `ctk_doctor` 与实时占用面板。[`87b35fe`]
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # dsh-context-toolkit
2
2
 
3
- > [简体中文](./README.zh.md) | **English**
3
+ > [简体中文](./README.zh.md) | **English** | [Changelog](./CHANGELOG.md)
4
4
 
5
5
  A DeepSeek Harness bundle whose single job is **keeping bulk output out of the conversation**.
6
6
 
@@ -115,7 +115,7 @@ Tool switches: `tools.execute`, `tools.executeFile`, `tools.batchExecute`, `tool
115
115
  The offline suites need no DSH runtime:
116
116
 
117
117
  ```bash
118
- node smoke.mjs # 85 checks: contract, runtime discovery, tools, gate (segment matching, host-aligned path base, script_path and host-fetcher coverage), output slimming with reversible retrieval and retention, resource disposal, knowledge base, observability, version reporting
118
+ node smoke.mjs # 87 checks: contract, runtime discovery, tools, gate (segment matching, host-aligned path base, script_path and host-fetcher coverage), output slimming with reversible retrieval and retention, resource disposal, knowledge base, observability, version reporting
119
119
  node test-client.mjs # 27 checks: browser-half contract and bilingual dictionary parity
120
120
  node test-package.mjs # 20 checks: publish manifest and the zero-dependency constraint
121
121
  node release-check.mjs # 30 checks: release readiness (manifest, deps, tool count, gate matching, version, SHA coverage, no hardcoded paths)
@@ -147,6 +147,7 @@ At `tools/post-execute`, the strategy follows the shape:
147
147
  - **`ctk_execute` timeouts are hard kills**: the child is terminated and `timedOut` is reported.
148
148
  - **A single run captures at most 256 KB**; anything beyond is counted but not retained. The text handed back is additionally capped at 4000 characters, so even a 1 MB single-line output cannot enter the conversation whole.
149
149
  - **The knowledge base deduplicates by path**: filing the same path again replaces the earlier copy.
150
+ - **Output a tool already truncated is not compressed again.** A body that ends in `[output truncated; full output: …]`, or in a standalone `~~~` line from the sandbox capture cap, is treated as work already done: this layer skips it and counts the reason in the ledger's "skip reasons" line.
150
151
  - **The gate is precise, not exhaustive.** A flood tool is refused only when it is the command word of some segment — after exec prefixes (`sudo`, `exec`, …) and path/extension normalisation are stripped — and `script_path` is read up to the first 256 KB. A tool a program invokes internally (`python -c 'import wget'`) is outside its remit.
151
152
  - **Paths resolve the way the host resolves them.** For `script_path` and for `read`'s `file_path`, a relative path is resolved against the session working directory — the same base the host uses to open the file. A gate reading that string against its own cwd would miss anything the host can still run.
152
153
 
package/README.zh.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 上下文工具箱(dsh-context-toolkit)
2
2
 
3
- > **简体中文** | [English](./README.md)
3
+ > **简体中文** | [English](./README.md) | [更新说明](./CHANGELOG.zh.md)
4
4
 
5
5
  一个给 DeepSeek Harness 用的插件:**让大块输出不进对话**。
6
6
 
@@ -116,7 +116,7 @@ await ctk_search({ queries: ['rate limit', 'auth'] }); // 只取相关片段
116
116
  离线套件不依赖 DSH 运行时:
117
117
 
118
118
  ```bash
119
- node smoke.mjs # 85 项:契约、运行时探测、工具、门禁(分段匹配 + 路径基准对齐 + script_path 与取网页工具覆盖)、输出瘦身与可逆取回及保留期、资源释放、知识库、观测、版本自述
119
+ node smoke.mjs # 87 项:契约、运行时探测、工具、门禁(分段匹配 + 路径基准对齐 + script_path 与取网页工具覆盖)、输出瘦身与可逆取回及保留期、资源释放、知识库、观测、版本自述
120
120
  node test-client.mjs # 27 项:浏览器半体契约与双语字典一致性
121
121
  node test-package.mjs # 20 项:发布清单与零依赖约束
122
122
  node release-check.mjs # 30 项:发布就绪(清单/依赖/工具数/门禁匹配/版本/SHA 覆盖/无硬编码路径)
@@ -148,6 +148,7 @@ node release-check.mjs # 30 项:发布就绪(清单/依赖/工具数/门禁
148
148
  - **`ctk_execute` 的超时是硬杀**:超时会终止子进程并把 `timedOut` 标出来。
149
149
  - **单次捕获上限 256 KB**,超出部分只计数不保留;回给对话的文本另有 4000 字符硬帽,因此 1 MB 的单行输出也不会整段进入上下文。
150
150
  - **知识库按路径去重**:同一路径再次归档是替换,不是追加。
151
+ - **已被上游截断的输出不再压缩**:上游工具自行截断并落盘的产物(`[output truncated; full output: …]`)与沙箱捕获上限的收尾(独立的 `~~~` 行)都视为已完成的工作,本层跳过并在台账「跳过原因」中计数。
151
152
  - **门禁精确而非穷尽**:只有当洪水工具是某个分段的「命令词」时才拒绝(会先剥掉 `sudo`/`exec` 等前缀、归一化路径与 `.exe` 等扩展名),`script_path` 最多读取前 256 KB。程序内部发起的调用(如 `python -c 'import wget'`)不在其职责内。
152
153
  - **路径基准与宿主一致**:门禁读取 `script_path` 与 `read` 的 `file_path` 时,按**会话工作目录**解析相对路径(与宿主实际打开文件的基准相同);否则「宿主能跑、门禁看不见」就是一条现成的绕过路径。
153
154
 
package/index.js CHANGED
@@ -271,6 +271,8 @@ export function apply(ctx, rawConfig) {
271
271
  slimmedBytes: 0,
272
272
  slimmedTokens: 0,
273
273
  slimSkipped: 0,
274
+ /** Why a body was left alone — "already truncated upstream" is a saving, not a miss. */
275
+ slimSkips: {},
274
276
  slimStrategies: {},
275
277
  blobsStored: 0,
276
278
  calls: {},
@@ -544,7 +546,15 @@ function registerOutputSlimmer(ctx, config, stats, store, log) {
544
546
  rewritten.push(block)
545
547
  continue
546
548
  }
547
- const outcome = slimText(block.text, { limits, isError })
549
+ // onSkip records why a body was exempted. "The tool already truncated it" is a
550
+ // legitimate outcome and belongs in the ledger beside the ones that saved bytes;
551
+ // otherwise the only visible number is how often this layer did nothing.
552
+ const outcome = slimText(block.text, {
553
+ limits,
554
+ isError,
555
+ label: name,
556
+ onSkip: (why) => { stats.slimSkips[why] = (stats.slimSkips[why] || 0) + 1 },
557
+ })
548
558
  if (!outcome) { stats.slimSkipped += 1; rewritten.push(block); continue }
549
559
 
550
560
  let marker = ''
@@ -1447,6 +1457,7 @@ function registerObservabilityTools(ctx, config, stats, store, storageDir, log)
1447
1457
  '- 沙箱保留 bytesSandboxed: ' + stats.bytesSandboxed + ' 字节(子进程产出但只回结论的部分)',
1448
1458
  '- 已归档 bytesIndexed: ' + stats.bytesIndexed + ' 字节(存入知识库、未留在对话里)',
1449
1459
  '- 输出瘦身 slimmed: ' + stats.slimmed + ' 块(' + stats.slimmedBytes + ' 字节 ≈ ' + stats.slimmedTokens + ' tokens 未进入上下文;跳过的块 ' + stats.slimSkipped + ')',
1460
+ '- 跳过原因: ' + (Object.keys(stats.slimSkips).length ? Object.keys(stats.slimSkips).map((k) => k + '×' + stats.slimSkips[k]).join(' · ') : '(无)'),
1450
1461
  '- 合计 kept out of context: ' + keptOut + ' 字节 ≈ ' + Math.round(keptOut / 4) + ' tokens(按 4 字节/token 估算)',
1451
1462
  '',
1452
1463
  '### 知识库 / knowledge base',
package/lib/compress.js CHANGED
@@ -47,6 +47,26 @@ function looksLikeNumberedSource(text) {
47
47
  return lines.length > 4 && numbered / lines.length > 0.5
48
48
  }
49
49
 
50
+ /**
51
+ * Was this output already truncated upstream, by the tool that produced it?
52
+ *
53
+ * Two markers are in the wild on this machine. A torn subprocess output ends with a
54
+ * bracket notice naming the spill file; the ctk_* sandbox caps what it hands back and
55
+ * ends with a tilde line. In both cases the bytes that would have flooded the context
56
+ * are already gone and already stored elsewhere, so compressing again cannot save
57
+ * anything — it only stores a second copy of the same original and claims the same
58
+ * saving twice. Telling them apart is the difference between a gate and a gate that
59
+ * bills the same traffic twice.
60
+ */
61
+ export function isAlreadyTruncated(text) {
62
+ const source = String(text == null ? '' : text)
63
+ // The notice is a whole line, so anchor it: a passing mention of the phrase inside
64
+ // prose must not exempt the body it appears in.
65
+ if (/^\[output truncated; full output: .+\]$/m.test(source)) return 'upstream spill notice'
66
+ if (/^~+$/m.test(source)) return 'sandbox capture cap'
67
+ return null
68
+ }
69
+
50
70
  /** What kind of output is this? The strategy follows the shape, not a guess. */
51
71
  export function classify(text) {
52
72
  const source = String(text || '')
@@ -156,6 +176,14 @@ export function slimText(text, options = {}) {
156
176
  const floor = options.isError ? limits.errorMinBytes : limits.minBytes
157
177
  if (originalBytes < floor) return null
158
178
 
179
+ // Exempt before any strategy runs: work already done upstream is not work saved here.
180
+ // Reported through onSkip so the caller can count it instead of silently ignoring it.
181
+ const truncatedBy = isAlreadyTruncated(source)
182
+ if (truncatedBy) {
183
+ if (typeof options.onSkip === 'function') options.onSkip(truncatedBy)
184
+ return null
185
+ }
186
+
159
187
  const kind = classify(source)
160
188
  if (kind === 'code' || kind === 'empty') return null
161
189
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-context-toolkit",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "DeepSeek Harness bundle: native ctk_* sandbox tools, a routing gate that keeps bulk output out of the transcript, and a local full-text knowledge base. No third-party runtime dependencies.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -27,6 +27,8 @@
27
27
  "cordis.patch.yml",
28
28
  "README.md",
29
29
  "README.zh.md",
30
+ "CHANGELOG.md",
31
+ "CHANGELOG.zh.md",
30
32
  "LICENSE",
31
33
  "locale/*.json",
32
34
  "icon.svg"