dsh-compact-arsenal 0.4.0 → 0.5.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/CHANGELOG.md +26 -0
- package/index.js +65 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,32 @@ All notable changes to this project are documented here. The format follows
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [0.5.0] — 2026-11-11
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **Reasoning compression could never land on any published DSH release.**
|
|
12
|
+
Reported in [#1](https://github.com/TacitSay/dsh-compact-arsenal/issues/1):
|
|
13
|
+
the log enforces two mutually exclusive rules on a surface replacement — every
|
|
14
|
+
shadowed node must be cited, and `assistant/message` may not cite anything.
|
|
15
|
+
Rewriting an assistant node in place is therefore impossible, and every
|
|
16
|
+
reasoning attempt failed with `missing <seq>` while its model call had already
|
|
17
|
+
been paid for. Reasoning now collapses into a `user/message` checkpoint citing
|
|
18
|
+
the node it replaces, the same shape the shipped compaction backends use.
|
|
19
|
+
- **`totalPass` cited only the nodes it fed the model.** Nodes filtered out
|
|
20
|
+
before summarization (system, developer, and turn-less nodes, including this
|
|
21
|
+
plugin's own earlier checkpoints) still fall inside `[startSeq, endSeq]` and
|
|
22
|
+
were reported as missing. The whole surface span is cited now.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- Pre-flight shape check: an impossible replacement is detected before the model
|
|
27
|
+
call, so a deterministic rejection no longer spends a request.
|
|
28
|
+
- `test/surface-shape.test.mjs` runs against the packaged `dsh-session` and pins
|
|
29
|
+
the accepted replacement shape for both node types.
|
|
30
|
+
- Source assertions now cover the reasoning shape, the whole-span citation, and
|
|
31
|
+
the pre-flight check (17 checks).
|
|
32
|
+
|
|
7
33
|
## [0.4.0] — 2026-11-11
|
|
8
34
|
|
|
9
35
|
### Fixed
|
package/index.js
CHANGED
|
@@ -31,7 +31,7 @@ export const name = 'compact-arsenal'
|
|
|
31
31
|
* Bumped in lockstep with `package.json` so the panel can show which build is
|
|
32
32
|
* actually running — a stale install is otherwise invisible.
|
|
33
33
|
*/
|
|
34
|
-
export const PACKAGE_VERSION = '0.
|
|
34
|
+
export const PACKAGE_VERSION = '0.5.1'
|
|
35
35
|
|
|
36
36
|
/** Host services this plugin reads; all ship with dsh-base or dsh-web-app. */
|
|
37
37
|
export const inject = ['sessions', 'tokenMeter', 'webServer', 'configEditor']
|
|
@@ -430,6 +430,19 @@ async function totalPass(ctx, session, total, turn, signal, stats, archive) {
|
|
|
430
430
|
if (picked.length < 4) return
|
|
431
431
|
|
|
432
432
|
const transcript = picked.map((node) => transcriptLine(node.event)).join('\n')
|
|
433
|
+
|
|
434
|
+
// The replacement must cite EVERY surface node inside the replaced span, not
|
|
435
|
+
// just the ones we fed the model: filters above skip system/developer nodes
|
|
436
|
+
// and nodes without a turn, and those still sit inside [startSeq, endSeq].
|
|
437
|
+
const startSeq = picked[0].seq
|
|
438
|
+
const endSeq = picked[picked.length - 1].seq
|
|
439
|
+
const surface = [...session.surface.nodes]
|
|
440
|
+
const startIdx = surface.indexOf(startSeq)
|
|
441
|
+
const endIdx = surface.indexOf(endSeq)
|
|
442
|
+
if (startIdx < 0 || endIdx < startIdx) return
|
|
443
|
+
if (startIdx === 0 && session.eventAt(surface[0])?.type === 'system/message') return
|
|
444
|
+
const shadowed = surface.slice(startIdx, endIdx + 1)
|
|
445
|
+
|
|
433
446
|
const routed = session.requestContext?.() ?? {}
|
|
434
447
|
const summaryCfg = {
|
|
435
448
|
...total,
|
|
@@ -446,8 +459,6 @@ async function totalPass(ctx, session, total, turn, signal, stats, archive) {
|
|
|
446
459
|
}
|
|
447
460
|
if (summary === '') return
|
|
448
461
|
|
|
449
|
-
const startSeq = picked[0].seq
|
|
450
|
-
const endSeq = picked[picked.length - 1].seq
|
|
451
462
|
try {
|
|
452
463
|
session.append(
|
|
453
464
|
'user/message',
|
|
@@ -459,7 +470,7 @@ async function totalPass(ctx, session, total, turn, signal, stats, archive) {
|
|
|
459
470
|
text: `This is an automatically generated checkpoint condensing an earlier span of the conversation to free up context. Treat the captured context as established background and build on it without restating it.\n\n<compacted-summary>\n${summary}\n</compacted-summary>`,
|
|
460
471
|
}],
|
|
461
472
|
},
|
|
462
|
-
{ surfaceOp: { op: 'replace', startSeq, endSeq }, sourceEventSeqs:
|
|
473
|
+
{ surfaceOp: { op: 'replace', startSeq, endSeq }, sourceEventSeqs: shadowed },
|
|
463
474
|
)
|
|
464
475
|
stats.total = (stats.total ?? 0) + 1
|
|
465
476
|
stats.chars += used
|
|
@@ -640,7 +651,16 @@ function currentPosition(session, event, currentTurn) {
|
|
|
640
651
|
}
|
|
641
652
|
|
|
642
653
|
/**
|
|
643
|
-
*
|
|
654
|
+
* Rewrite one node's text, using the only shape the log accepts for each type.
|
|
655
|
+
*
|
|
656
|
+
* `tool/result` may be replaced in place because it can cite the node it
|
|
657
|
+
* shadows. `assistant/message` cannot: the log rejects any `sourceEventSeqs` on
|
|
658
|
+
* it ("assistant/message embeds its source stream"), while every replacement
|
|
659
|
+
* must cite all shadowed nodes — so an in-place assistant rewrite is impossible
|
|
660
|
+
* on every published version. Reasoning therefore collapses into a
|
|
661
|
+
* `user/message` checkpoint covering the whole replaced span, which is the same
|
|
662
|
+
* shape the shipped compaction backends use.
|
|
663
|
+
*
|
|
644
664
|
* @param {any} session - live session.
|
|
645
665
|
* @param {any} event - the surface event being replaced.
|
|
646
666
|
* @param {number} seq - its surface seq.
|
|
@@ -650,26 +670,46 @@ function currentPosition(session, event, currentTurn) {
|
|
|
650
670
|
* @returns {void}
|
|
651
671
|
*/
|
|
652
672
|
function applyRewrittenText(session, event, seq, kind, text, currentTurn) {
|
|
653
|
-
const blocks = event.data?.message?.content
|
|
654
|
-
const wanted = kind === 'tools' ? 'text' : 'reasoning'
|
|
655
|
-
const rich = blocks.filter((block) => block?.type !== wanted)
|
|
656
|
-
const message = { ...event.data.message, content: [{ type: wanted, text }, ...rich] }
|
|
657
|
-
const position = currentPosition(session, event, currentTurn)
|
|
658
673
|
if (kind === 'tools') {
|
|
674
|
+
const blocks = event.data?.message?.content ?? []
|
|
675
|
+
const rich = blocks.filter((block) => block?.type !== 'text')
|
|
676
|
+
const message = { ...event.data.message, content: [{ type: 'text', text }, ...rich] }
|
|
659
677
|
session.append('tool/result', {
|
|
660
|
-
...
|
|
678
|
+
...currentPosition(session, event, currentTurn),
|
|
661
679
|
message,
|
|
662
680
|
...(event.data.error === undefined ? {} : { error: event.data.error }),
|
|
663
681
|
...(event.data.meta === undefined ? {} : { meta: event.data.meta }),
|
|
664
682
|
}, { surfaceOp: { op: 'replace', startSeq: seq, endSeq: seq }, sourceEventSeqs: [seq] })
|
|
665
683
|
return
|
|
666
684
|
}
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
685
|
+
|
|
686
|
+
// Reasoning: replace the node with a user-role checkpoint that cites it.
|
|
687
|
+
session.append('user/message', {
|
|
688
|
+
role: 'user',
|
|
689
|
+
source: { kind: 'user' },
|
|
690
|
+
content: [{ type: 'text', text }],
|
|
691
|
+
}, { surfaceOp: { op: 'replace', startSeq: seq, endSeq: seq }, sourceEventSeqs: [seq] })
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Whether one section can legally replace a node in this session right now.
|
|
696
|
+
*
|
|
697
|
+
* The check is deliberately cheap and local: it mirrors the log's own rules
|
|
698
|
+
* (a replacement must cite the node it shadows, and `assistant/message` may not
|
|
699
|
+
* cite anything) so an impossible write is detected before a model call.
|
|
700
|
+
*
|
|
701
|
+
* @param {any} session - live session.
|
|
702
|
+
* @param {'reasoning' | 'tools'} kind - section kind.
|
|
703
|
+
* @param {number | undefined} seq - the node that would be replaced.
|
|
704
|
+
* @returns {boolean} true when the replacement shape is allowed.
|
|
705
|
+
*/
|
|
706
|
+
function canReplace(session, kind, seq) {
|
|
707
|
+
if (seq === undefined) return false
|
|
708
|
+
const surface = session.surface?.nodes
|
|
709
|
+
if (!Array.isArray(surface) || !surface.includes(seq)) return false
|
|
710
|
+
if (kind === 'tools') return session.eventAt?.(seq)?.type === 'tool/result'
|
|
711
|
+
// Reasoning collapses into a user/message checkpoint, which may cite the node.
|
|
712
|
+
return kind === 'reasoning'
|
|
673
713
|
}
|
|
674
714
|
|
|
675
715
|
/**
|
|
@@ -712,6 +752,14 @@ async function compressSection(ctx, session, policy, kind, seqs, signal, archive
|
|
|
712
752
|
}
|
|
713
753
|
if (totalChars < minTotal) return { items: 0, chars: 0, failed: 0, archived: 0 }
|
|
714
754
|
|
|
755
|
+
// A rejection here is deterministic: the log will refuse the same shape every
|
|
756
|
+
// time. Probing with one throwaway append keeps a doomed batch from spending a
|
|
757
|
+
// model call, so `mode: model` cannot burn requests on writes that cannot land.
|
|
758
|
+
if (!canReplace(session, kind, candidates[0].seq)) {
|
|
759
|
+
lastFailure = `${kind}: surface shape rejected before any model call`
|
|
760
|
+
return { items: 0, chars: 0, failed: 1, archived: 0 }
|
|
761
|
+
}
|
|
762
|
+
|
|
715
763
|
const routed = session.requestContext?.() ?? {}
|
|
716
764
|
const model = policy.model ?? {}
|
|
717
765
|
const modelCfg = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-compact-arsenal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Configurable context compaction for DeepSeek Harness: three independent sections — whole-context checkpoint, reasoning compression and tool-output pruning, each with its own switch, threshold and method, plus a left-sidebar panel.",
|
|
5
5
|
"icon": "icon.svg",
|
|
6
6
|
"author": "TacitSay",
|