@logictan/dsh-verbatim-compaction 0.4.0 → 0.5.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 +4 -0
- package/lib/index.js +67 -4
- package/lib/types/index.d.ts +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -69,6 +69,10 @@
|
|
|
69
69
|
- **工具调用从 `assistant/message` 的 block 里取**。日志里没有独立的 `tool/call` 事件类型,按它过滤会一条都取不到。
|
|
70
70
|
- **提取失败就退回逐字正文**。模型不可达、超时、输出为空时,该轮保留原来的助手正文;压缩本身不能因此失败,否则会话死于上下文溢出,比丢细节更糟。降级是逐轮的:一批里某一轮没答,只有那一轮退回正文。
|
|
71
71
|
|
|
72
|
+
提取结果按内容哈希缓存,同一次会话内复用。`digestSource` 每次压缩都从原始日志重建,而原始事件永远留在日志里,所以同一轮会被反复读到。实测那个压缩了 33 次的会话:共提取 142 轮,其中 **129 轮(91%)在更早的压缩里已经提取过**;全机 370 个会话、456 次压缩里,**81% 的压缩工作量是这种重复**(有压缩的 88 个会话中 70 个压缩过 2 次以上)。
|
|
73
|
+
|
|
74
|
+
缓存的键是提取材料的哈希(用户请求 + 助手正文 + 工具轨迹),不是 slot 位置:位置会随历史增长而错位,内容变了哈希就变,不会读到过期块。缓存是进程内的,重启即失效——代价只是重新提取一次,与加缓存前行为一致。
|
|
75
|
+
|
|
72
76
|
提取按批调用:每 8 轮一次,一轮输出一个 `=== TURN n ===` 块。早期实现是每轮一次调用,122 轮就是 131 次往返,单轮 400 token 的输出上限乘以 131 就可能超过整个摘要上限。批处理把往返压到约 17 次,并共享 prompt 前缀以复用 provider 的 KV cache。
|
|
73
77
|
|
|
74
78
|
用户原话在布局上**先占额度**,提取块不得挤占。实测那个 122 轮会话:区间内真人原话共 469 字符(117 token),而每轮一块 420 字符的提取共 55020 字符——提取是正文的 117 倍,任何让二者竞争的布局都会把正文挤掉,而正文是「请用pixi管理开发环境」这类永不过期的约束,提取描述的却是已经做完的工作。
|
package/lib/index.js
CHANGED
|
@@ -434,6 +434,39 @@ function withExtractions(sections, extracted) {
|
|
|
434
434
|
function turnKey(sectionIndex, turnIndex) {
|
|
435
435
|
return `${sectionIndex}:${turnIndex}`;
|
|
436
436
|
}
|
|
437
|
+
/**
|
|
438
|
+
* Cache key for one slot's extraction material: a hash of everything the model
|
|
439
|
+
* would be shown.
|
|
440
|
+
*
|
|
441
|
+
* The material, not the slot's position or turn number: positions shift as
|
|
442
|
+
* history grows and numbers repeat, but identical material must produce an
|
|
443
|
+
* identical extraction. Hashing the exact string `buildExtractionMaterial`
|
|
444
|
+
* returns means a change anywhere in the user request, the prose, or the tool
|
|
445
|
+
* trace invalidates the entry — nothing can silently go stale.
|
|
446
|
+
*
|
|
447
|
+
* FNV-1a, 32-bit, two seeds. Not cryptographic: the risk here is an accidental
|
|
448
|
+
* collision between two turns, and 64 bits over a few hundred entries makes that
|
|
449
|
+
* vanishingly unlikely.
|
|
450
|
+
*
|
|
451
|
+
* @param events - one slot's events, in log order.
|
|
452
|
+
* @returns a stable hex key.
|
|
453
|
+
*/
|
|
454
|
+
function materialKey(events) {
|
|
455
|
+
const prose = events
|
|
456
|
+
.filter((event) => event.type === 'assistant/message')
|
|
457
|
+
.map((event) => textOf(event))
|
|
458
|
+
.filter((text) => text.length > 0)
|
|
459
|
+
.join('\n');
|
|
460
|
+
const material = buildExtractionMaterial(events, prose);
|
|
461
|
+
let a = 0x811c9dc5;
|
|
462
|
+
let b = 0x01000193;
|
|
463
|
+
for (let index = 0; index < material.length; index += 1) {
|
|
464
|
+
const code = material.charCodeAt(index);
|
|
465
|
+
a = Math.imul(a ^ code, 0x01000193) >>> 0;
|
|
466
|
+
b = Math.imul(b ^ (code + index), 0x85ebca6b) >>> 0;
|
|
467
|
+
}
|
|
468
|
+
return `${a.toString(16)}${b.toString(16)}`;
|
|
469
|
+
}
|
|
437
470
|
/**
|
|
438
471
|
* Render one section to digest text, truncating assistant prose to `capChars`.
|
|
439
472
|
*
|
|
@@ -778,6 +811,23 @@ async function extractBatch(ctx, agent, batch, signal) {
|
|
|
778
811
|
* subclass cannot diverge from the host's transaction semantics.
|
|
779
812
|
*/
|
|
780
813
|
export default class VerbatimCompactionEngine extends BasicCompactionEngine {
|
|
814
|
+
/**
|
|
815
|
+
* Extraction results, keyed by a hash of the material they were derived from.
|
|
816
|
+
*
|
|
817
|
+
* `digestSource` rebuilds from the raw log on every compaction, and raw events
|
|
818
|
+
* never leave the log, so the same turn is re-read on every later compaction.
|
|
819
|
+
* Measured on the session that compacted 33 times: 142 turns extracted, 129 of
|
|
820
|
+
* them (91%) already extracted earlier. Across all 370 local sessions, 81% of
|
|
821
|
+
* compaction work is that repeat.
|
|
822
|
+
*
|
|
823
|
+
* Keyed by content, not by slot position: a position holds different content
|
|
824
|
+
* once history grows, so a positional key would serve a stale block. A turn
|
|
825
|
+
* whose prose changed hashes differently and is extracted again.
|
|
826
|
+
*
|
|
827
|
+
* Process-local and bounded. A restart loses it, costing one re-extraction —
|
|
828
|
+
* the same behaviour as before this cache existed.
|
|
829
|
+
*/
|
|
830
|
+
extractionCache = new Map();
|
|
781
831
|
/**
|
|
782
832
|
* Build the replacement checkpoint content from the session log.
|
|
783
833
|
*
|
|
@@ -873,13 +923,26 @@ export default class VerbatimCompactionEngine extends BasicCompactionEngine {
|
|
|
873
923
|
}
|
|
874
924
|
}
|
|
875
925
|
const extracted = new Map();
|
|
876
|
-
|
|
926
|
+
const missing = [];
|
|
927
|
+
for (const slot of slots) {
|
|
928
|
+
const cached = this.extractionCache.get(materialKey(slot.events));
|
|
929
|
+
if (cached === undefined)
|
|
930
|
+
missing.push(slot);
|
|
931
|
+
else
|
|
932
|
+
extracted.set(slot.key, cached);
|
|
933
|
+
}
|
|
934
|
+
for (let index = 0; index < missing.length; index += TURNS_PER_CALL) {
|
|
877
935
|
if (signal?.aborted === true)
|
|
878
936
|
break;
|
|
879
|
-
const batch =
|
|
937
|
+
const batch = missing.slice(index, index + TURNS_PER_CALL);
|
|
880
938
|
const blocks = await extractBatch(this.ctx, agent, batch, signal);
|
|
881
|
-
for (const
|
|
882
|
-
|
|
939
|
+
for (const slot of batch) {
|
|
940
|
+
const block = blocks.get(slot.key);
|
|
941
|
+
if (block === undefined)
|
|
942
|
+
continue;
|
|
943
|
+
extracted.set(slot.key, block);
|
|
944
|
+
this.extractionCache.set(materialKey(slot.events), block);
|
|
945
|
+
}
|
|
883
946
|
}
|
|
884
947
|
return extracted;
|
|
885
948
|
}
|
package/lib/types/index.d.ts
CHANGED
|
@@ -231,6 +231,23 @@ export declare function buildExtractionMaterial(turn: readonly SessionEvent[], p
|
|
|
231
231
|
* subclass cannot diverge from the host's transaction semantics.
|
|
232
232
|
*/
|
|
233
233
|
export default class VerbatimCompactionEngine extends BasicCompactionEngine {
|
|
234
|
+
/**
|
|
235
|
+
* Extraction results, keyed by a hash of the material they were derived from.
|
|
236
|
+
*
|
|
237
|
+
* `digestSource` rebuilds from the raw log on every compaction, and raw events
|
|
238
|
+
* never leave the log, so the same turn is re-read on every later compaction.
|
|
239
|
+
* Measured on the session that compacted 33 times: 142 turns extracted, 129 of
|
|
240
|
+
* them (91%) already extracted earlier. Across all 370 local sessions, 81% of
|
|
241
|
+
* compaction work is that repeat.
|
|
242
|
+
*
|
|
243
|
+
* Keyed by content, not by slot position: a position holds different content
|
|
244
|
+
* once history grows, so a positional key would serve a stale block. A turn
|
|
245
|
+
* whose prose changed hashes differently and is extracted again.
|
|
246
|
+
*
|
|
247
|
+
* Process-local and bounded. A restart loses it, costing one re-extraction —
|
|
248
|
+
* the same behaviour as before this cache existed.
|
|
249
|
+
*/
|
|
250
|
+
private readonly extractionCache;
|
|
234
251
|
/**
|
|
235
252
|
* Build the replacement checkpoint content from the session log.
|
|
236
253
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logictan/dsh-verbatim-compaction",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Deterministic compaction backend that keeps conversation prose verbatim in the checkpoint instead of re-summarizing it: user and assistant text survive word for word, tool results collapse to one-line pointers, reasoning is never read, and no model call is made.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|