dsh-context-toolkit 0.3.0 → 0.3.2
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 +2 -1
- package/README.zh.md +2 -1
- package/index.js +33 -3
- package/lib/store.js +25 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -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 #
|
|
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
|
|
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)
|
|
@@ -139,6 +139,7 @@ At `tools/post-execute`, the strategy follows the shape:
|
|
|
139
139
|
**Two gates**: a candidate must be at most 72% of the original bytes *and* have a strictly lower token estimate, or it is dropped — no saving means no claim, and the ledger never overstates.
|
|
140
140
|
|
|
141
141
|
**Reversibility is the red line**: every lossy path stores the exact original first and injects a `[ctk_retrieve id=…]` marker, read back with `ctk_retrieve`. The originals live in the **same database as the FTS5 index** — one file, two jobs: FTS5 finds sections, blobs restore them.
|
|
142
|
+
- **Retention**: `slimOriginalTtlMinutes` (default 1440 = one day, 0 = forever) is swept once on every load; when a marker cannot be delivered the blob is rolled back by id, so an unrelated original is never taken with it.
|
|
142
143
|
|
|
143
144
|
## Known limits
|
|
144
145
|
|
package/README.zh.md
CHANGED
|
@@ -116,7 +116,7 @@ await ctk_search({ queries: ['rate limit', 'auth'] }); // 只取相关片段
|
|
|
116
116
|
离线套件不依赖 DSH 运行时:
|
|
117
117
|
|
|
118
118
|
```bash
|
|
119
|
-
node smoke.mjs #
|
|
119
|
+
node smoke.mjs # 85 项:契约、运行时探测、工具、门禁(分段匹配 + 路径基准对齐 + script_path 与取网页工具覆盖)、输出瘦身与可逆取回及保留期、资源释放、知识库、观测、版本自述
|
|
120
120
|
node test-client.mjs # 27 项:浏览器半体契约与双语字典一致性
|
|
121
121
|
node test-package.mjs # 20 项:发布清单与零依赖约束
|
|
122
122
|
node release-check.mjs # 30 项:发布就绪(清单/依赖/工具数/门禁匹配/版本/SHA 覆盖/无硬编码路径)
|
|
@@ -140,6 +140,7 @@ node release-check.mjs # 30 项:发布就绪(清单/依赖/工具数/门禁
|
|
|
140
140
|
**双重门**:候选必须同时满足「字节 ≤ 72%」且「估算 token 严格下降」,否则放弃——压不动就不压,台账绝不虚报节省。
|
|
141
141
|
|
|
142
142
|
**可逆红线**:任何有损路径先把原文**逐字节**存好并注入 `[ctk_retrieve id=…]` 标记,用 `ctk_retrieve` 取回。原文与 FTS5 索引同在**同一个数据库**(一个文件两个职责:FTS5 找段落,blob 逐字节还原)。
|
|
143
|
+
- **原文保留期**:`slimOriginalTtlMinutes`(默认 1440 = 一天,0 = 永久)由插件在**每次加载时**执行一次清理;标记投递失败时按 id 精确回滚,不会牵连其他原文。
|
|
143
144
|
|
|
144
145
|
## 已知边界
|
|
145
146
|
|
package/index.js
CHANGED
|
@@ -284,6 +284,19 @@ export function apply(ctx, rawConfig) {
|
|
|
284
284
|
log.warn('knowledge base unavailable: ' + String((error && error.message) || error))
|
|
285
285
|
}
|
|
286
286
|
|
|
287
|
+
// Retention is enforced here, once per load, because the alternative was a setting
|
|
288
|
+
// the user could see and the code never read: slimOriginalTtlMinutes was exposed in
|
|
289
|
+
// the settings page while nothing consumed it, so stored originals accumulated
|
|
290
|
+
// forever and the promise the panel made was not kept.
|
|
291
|
+
if (store) {
|
|
292
|
+
try {
|
|
293
|
+
const swept = store.purgeBlobsOlderThan(Number(config.slimOriginalTtlMinutes))
|
|
294
|
+
if (swept.removed > 0) log.info('retention sweep removed ' + swept.removed + ' stored original(s)')
|
|
295
|
+
} catch (error) {
|
|
296
|
+
log.warn('retention sweep failed: ' + String((error && error.message) || error))
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
287
300
|
registerRoutingGate(ctx, config, stats)
|
|
288
301
|
registerOutputSlimmer(ctx, config, stats, store, log)
|
|
289
302
|
registerTools(ctx, config, stats, storageDir, log)
|
|
@@ -559,7 +572,10 @@ function registerOutputSlimmer(ctx, config, stats, store, log) {
|
|
|
559
572
|
if (Buffer.byteLength(slimmed, 'utf8') >= outcome.originalBytes) {
|
|
560
573
|
stats.slimSkipped += 1
|
|
561
574
|
rewritten.push(block)
|
|
562
|
-
|
|
575
|
+
// The blob was written but the marker could not be delivered, so nothing will
|
|
576
|
+
// ever reference it. Removing this specific id is the point: the previous call
|
|
577
|
+
// passed { all: false } with no age, which purgeBlobs treats as "nothing to do".
|
|
578
|
+
if (marker) { try { store.purgeBlobs({ ids: [id] }) } catch { /* blob stays, harmless */ } }
|
|
563
579
|
continue
|
|
564
580
|
}
|
|
565
581
|
|
|
@@ -1086,11 +1102,16 @@ function registerKnowledgeTools(ctx, config, stats, store, log) {
|
|
|
1086
1102
|
}
|
|
1087
1103
|
}
|
|
1088
1104
|
stats.calls['ctk_retrieve'] = (stats.calls['ctk_retrieve'] || 0) + 1
|
|
1105
|
+
// isError must be present on every return: the host validates a tool's output
|
|
1106
|
+
// against the declared schema and rejects the call outright when it is missing.
|
|
1107
|
+
// Omitting it here made the whole tool unusable — the marker was injected, the
|
|
1108
|
+
// original was stored, and the read-back failed with "missing required property".
|
|
1089
1109
|
return {
|
|
1090
1110
|
text: '## ctk_retrieve ' + id + '\n'
|
|
1091
1111
|
+ '- bytes: ' + found.bytes + ' (exact original, restored verbatim)\n'
|
|
1092
1112
|
+ '- stored: ' + new Date(found.createdAtMs).toISOString() + '\n\n'
|
|
1093
1113
|
+ found.text,
|
|
1114
|
+
isError: false,
|
|
1094
1115
|
}
|
|
1095
1116
|
},
|
|
1096
1117
|
}, 'ctk_retrieve'))
|
|
@@ -1411,8 +1432,11 @@ function registerObservabilityTools(ctx, config, stats, store, storageDir, log)
|
|
|
1411
1432
|
description: 'Report this plugin\'s savings ledger and knowledge-base usage. Shows how many calls the routing gate refused, how many bytes the sandbox kept out of the conversation, how many bytes were filed, and the per-tool call counts.',
|
|
1412
1433
|
parameters: { type: 'object', properties: {}, additionalProperties: false },
|
|
1413
1434
|
async execute() {
|
|
1414
|
-
const kb = store ? store.stats() : { documents: 0, chunks: 0, bytes: 0 }
|
|
1415
|
-
|
|
1435
|
+
const kb = store ? store.stats() : { documents: 0, chunks: 0, bytes: 0, blobs: 0, blobBytes: 0 }
|
|
1436
|
+
// Slimmed bytes are kept out too — they were removed from output before it
|
|
1437
|
+
// reached the model. Leaving them out understated the ledger by exactly the
|
|
1438
|
+
// amount this half of the plugin exists to save.
|
|
1439
|
+
const produced = stats.bytesSandboxed + stats.bytesIndexed + stats.deniedBytes + stats.slimmedBytes
|
|
1416
1440
|
const keptOut = produced
|
|
1417
1441
|
const uptimeMs = Date.now() - stats.startedAt
|
|
1418
1442
|
const lines = [
|
|
@@ -1422,12 +1446,15 @@ function registerObservabilityTools(ctx, config, stats, store, storageDir, log)
|
|
|
1422
1446
|
'- 门禁拦截 denied: ' + stats.denied + ' 次(约 ' + stats.deniedBytes + ' 字节未进入上下文)',
|
|
1423
1447
|
'- 沙箱保留 bytesSandboxed: ' + stats.bytesSandboxed + ' 字节(子进程产出但只回结论的部分)',
|
|
1424
1448
|
'- 已归档 bytesIndexed: ' + stats.bytesIndexed + ' 字节(存入知识库、未留在对话里)',
|
|
1449
|
+
'- 输出瘦身 slimmed: ' + stats.slimmed + ' 块(' + stats.slimmedBytes + ' 字节 ≈ ' + stats.slimmedTokens + ' tokens 未进入上下文;跳过的块 ' + stats.slimSkipped + ')',
|
|
1425
1450
|
'- 合计 kept out of context: ' + keptOut + ' 字节 ≈ ' + Math.round(keptOut / 4) + ' tokens(按 4 字节/token 估算)',
|
|
1426
1451
|
'',
|
|
1427
1452
|
'### 知识库 / knowledge base',
|
|
1428
1453
|
'- 文档 documents: ' + kb.documents,
|
|
1429
1454
|
'- 片段 chunks: ' + kb.chunks,
|
|
1430
1455
|
'- 原文字节 bytes: ' + kb.bytes,
|
|
1456
|
+
'- 可逆原文 blobs: ' + (kb.blobs || 0) + '(' + (kb.blobBytes || 0) + ' 字节,可用 ctk_retrieve 逐字节取回)',
|
|
1457
|
+
'- 原文保留期 retention: ' + (Number(config.slimOriginalTtlMinutes) > 0 ? Number(config.slimOriginalTtlMinutes) + ' 分钟(每次加载时清理过期项)' : '永久保留'),
|
|
1431
1458
|
'- 位置: ' + storageDir,
|
|
1432
1459
|
'',
|
|
1433
1460
|
'### 本插件注册的工具',
|
|
@@ -1499,6 +1526,9 @@ function registerObservabilityTools(ctx, config, stats, store, storageDir, log)
|
|
|
1499
1526
|
lines.push('- 门禁拦截 denied: ' + stats.denied + ' 次(' + stats.deniedBytes + ' 字节未进入上下文)')
|
|
1500
1527
|
lines.push('- 沙箱保留 bytesSandboxed: ' + stats.bytesSandboxed + ' 字节')
|
|
1501
1528
|
lines.push('- 已归档 bytesIndexed: ' + stats.bytesIndexed + ' 字节')
|
|
1529
|
+
lines.push('- 输出瘦身 slimmed: ' + stats.slimmed + ' 块(' + stats.slimmedBytes + ' 字节 ≈ ' + stats.slimmedTokens + ' tokens 未进入上下文)')
|
|
1530
|
+
lines.push('- 可逆原文 blobsStored: ' + stats.blobsStored + ' 份(可用 ctk_retrieve 逐字节取回)')
|
|
1531
|
+
if (stats.slimmed > 0) lines.push('- 瘦身策略: ' + Object.keys(stats.slimStrategies).map((k) => k + '×' + stats.slimStrategies[k]).join(' · '))
|
|
1502
1532
|
lines.push('- 已注册 registered: ' + stats.registered)
|
|
1503
1533
|
return { text: lines.join('\n'), isError: false }
|
|
1504
1534
|
},
|
package/lib/store.js
CHANGED
|
@@ -165,13 +165,27 @@ function makeApi(db, dbPath, storageDir) {
|
|
|
165
165
|
return { id: String(id), text: Buffer.from(row.body).toString('utf8'), bytes: Number(row.bytes) || 0, createdAtMs: Number(row.created_at_ms) || 0 }
|
|
166
166
|
},
|
|
167
167
|
|
|
168
|
-
/**
|
|
169
|
-
|
|
168
|
+
/**
|
|
169
|
+
* Drop stored originals: specific ids, everything older than an age, or all.
|
|
170
|
+
*
|
|
171
|
+
* The three modes answer different questions. A caller whose marker never reached
|
|
172
|
+
* the model removes the one id it just wrote; a retention policy removes by age; a
|
|
173
|
+
* reset removes all. Removing by id first means a rollback cannot take an unrelated
|
|
174
|
+
* original down with it.
|
|
175
|
+
*/
|
|
176
|
+
purgeBlobs({ ids = [], olderThanMs = 0, all = false } = {}) {
|
|
170
177
|
if (all) {
|
|
171
178
|
const n = db.prepare('SELECT COUNT(*) AS n FROM blobs').get().n
|
|
172
179
|
db.exec('DELETE FROM blobs;')
|
|
173
180
|
return { removed: Number(n) || 0 }
|
|
174
181
|
}
|
|
182
|
+
const list = (Array.isArray(ids) ? ids : [ids]).map((v) => String(v || '')).filter(Boolean)
|
|
183
|
+
if (list.length) {
|
|
184
|
+
const del = db.prepare('DELETE FROM blobs WHERE blob_id = ?')
|
|
185
|
+
let removed = 0
|
|
186
|
+
for (const id of list) removed += Number(del.run(id).changes) || 0
|
|
187
|
+
return { removed }
|
|
188
|
+
}
|
|
175
189
|
if (!(olderThanMs > 0)) return { removed: 0 }
|
|
176
190
|
const cutoff = Date.now() - olderThanMs
|
|
177
191
|
const n = db.prepare('SELECT COUNT(*) AS n FROM blobs WHERE created_at_ms < ?').get(cutoff).n
|
|
@@ -179,6 +193,15 @@ function makeApi(db, dbPath, storageDir) {
|
|
|
179
193
|
return { removed: Number(n) || 0 }
|
|
180
194
|
},
|
|
181
195
|
|
|
196
|
+
/** Drop originals older than a retention in minutes; 0 means keep forever. */
|
|
197
|
+
purgeBlobsOlderThan(minutes) {
|
|
198
|
+
const n = Number(minutes)
|
|
199
|
+
// An explicit 0 is a policy ("keep forever"), not an oversight, so it must not be
|
|
200
|
+
// read as "expire immediately".
|
|
201
|
+
if (!(n > 0)) return { removed: 0 }
|
|
202
|
+
return this.purgeBlobs({ olderThanMs: n * 60000 })
|
|
203
|
+
},
|
|
204
|
+
|
|
182
205
|
/** File one document, replacing any earlier version of the same path. */
|
|
183
206
|
indexDocument({ path, title, text, mtimeMs }) {
|
|
184
207
|
const abs = normalizeTarget(path)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-context-toolkit",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|