dsh-compact-arsenal 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/CHANGELOG.md +17 -0
- package/client.js +9 -0
- package/index.js +59 -8
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ 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.2.1] — 2026-11-10
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- The pressure gate now reads the provider-reported prompt size
|
|
12
|
+
(`ctx.tokenMeter.contextPressure()`), falling back to the heuristic estimate
|
|
13
|
+
only when no provider figure exists. The heuristic prices text at ~4
|
|
14
|
+
characters per token, which undercounts CJK and JSON by about 12%: a session
|
|
15
|
+
measured at 51.9% of its window was read as 45.4% and never tripped a 0.5
|
|
16
|
+
threshold.
|
|
17
|
+
- The panel shows both numbers (真实 / 估算) so a gate decision can be explained.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `docs/threshold-notes.md`: a field record of one session, the exact crossing
|
|
22
|
+
sample, every rewrite it produced, and the two measurement bases side by side.
|
|
23
|
+
|
|
7
24
|
## [0.2.0] — 2026-11-03
|
|
8
25
|
|
|
9
26
|
### Added
|
package/client.js
CHANGED
|
@@ -269,6 +269,7 @@ window.__ModuleLoader__.load({
|
|
|
269
269
|
const [lastFailure, setLastFailure] = useState(null)
|
|
270
270
|
const [archiveRoot, setArchiveRoot] = useState(null)
|
|
271
271
|
const [lastArchivePath, setLastArchivePath] = useState(null)
|
|
272
|
+
const [pressure, setPressure] = useState(null)
|
|
272
273
|
const timer = useRef(null)
|
|
273
274
|
const latest = useRef(emptyConfig)
|
|
274
275
|
|
|
@@ -285,6 +286,7 @@ window.__ModuleLoader__.load({
|
|
|
285
286
|
setLastFailure(body.lastFailure ?? null)
|
|
286
287
|
setArchiveRoot(body.archiveRoot ?? null)
|
|
287
288
|
setLastArchivePath(body.lastArchivePath ?? null)
|
|
289
|
+
setPressure(body.pressure ?? null)
|
|
288
290
|
setStatus(body.editable === false ? 'readonly' : 'ready')
|
|
289
291
|
})
|
|
290
292
|
.catch((error) => {
|
|
@@ -314,6 +316,7 @@ window.__ModuleLoader__.load({
|
|
|
314
316
|
setLastFailure(body.lastFailure ?? null)
|
|
315
317
|
if (body.archiveRoot) setArchiveRoot(body.archiveRoot)
|
|
316
318
|
if (body.lastArchivePath !== undefined) setLastArchivePath(body.lastArchivePath)
|
|
319
|
+
if (body.pressure !== undefined) setPressure(body.pressure)
|
|
317
320
|
setTotalApplied(body.totalApplied === undefined ? null : body.totalApplied)
|
|
318
321
|
setStatus('ready')
|
|
319
322
|
setMessage('已保存')
|
|
@@ -485,6 +488,12 @@ window.__ModuleLoader__.load({
|
|
|
485
488
|
? h('p', { key: 'fail', style: { margin: 0, fontSize: 11, color: WARN, lineHeight: 1.6 } },
|
|
486
489
|
`失败 ${stats.failed} 次(未落盘,不计入节省):${lastFailure || '原因未记录'}`)
|
|
487
490
|
: null,
|
|
491
|
+
pressure && typeof pressure.estimated === 'number' && pressure.estimated > 0
|
|
492
|
+
? h('p', { key: 'pressure', style: { margin: 0, fontSize: 11, color: MUTED, lineHeight: 1.6 } },
|
|
493
|
+
pressure.window
|
|
494
|
+
? `上下文压力:真实 ${(pressure.reported ?? pressure.estimated).toLocaleString()}(${((pressure.reported ?? pressure.estimated) / pressure.window * 100).toFixed(1)}%)· 估算 ${pressure.estimated.toLocaleString()}(${(pressure.estimated / pressure.window * 100).toFixed(1)}%)· 窗口 ${pressure.window.toLocaleString()}`
|
|
495
|
+
: `上下文压力:真实 ${(pressure.reported ?? pressure.estimated).toLocaleString()} · 估算 ${pressure.estimated.toLocaleString()}`)
|
|
496
|
+
: null,
|
|
488
497
|
archiveRoot
|
|
489
498
|
? h('p', { key: 'archive', style: { margin: 0, fontSize: 11, color: MUTED, lineHeight: 1.6, wordBreak: 'break-all' } },
|
|
490
499
|
`归档目录:${archiveRoot}${lastArchivePath ? `(最新:${lastArchivePath})` : ''}(共 ${stats.archived ?? 0} 份)`)
|
package/index.js
CHANGED
|
@@ -189,6 +189,11 @@ export function apply(ctx, rawConfig) {
|
|
|
189
189
|
/** Last turn already processed, per session, so one turn is rewritten at most once. */
|
|
190
190
|
const lastProcessedTurn = new Map()
|
|
191
191
|
const stats = { reasoning: 0, tools: 0, chars: 0, failed: 0, archived: 0 }
|
|
192
|
+
/**
|
|
193
|
+
* Last observed pressure inputs, so the panel can show both the provider's
|
|
194
|
+
* number and the heuristic estimate that the gate falls back to.
|
|
195
|
+
*/
|
|
196
|
+
const pressure = { reported: null, estimated: 0, window: null }
|
|
192
197
|
const archive = { root: archiveRoot(config.archiveDir), lastPath: null }
|
|
193
198
|
|
|
194
199
|
if (config.enabled) {
|
|
@@ -196,7 +201,7 @@ export function apply(ctx, rawConfig) {
|
|
|
196
201
|
const decision = await next()
|
|
197
202
|
if (decision?.kind !== 'enter') return decision
|
|
198
203
|
try {
|
|
199
|
-
rewrite(ctx, config, payload, lastProcessedTurn, stats, archive)
|
|
204
|
+
rewrite(ctx, config, payload, lastProcessedTurn, stats, archive, pressure)
|
|
200
205
|
} catch (error) {
|
|
201
206
|
// A rewrite failure must never break the step.
|
|
202
207
|
ctx.logger?.warn?.(`[compact-arsenal] rewrite skipped: ${error?.message ?? error}`)
|
|
@@ -207,7 +212,7 @@ export function apply(ctx, rawConfig) {
|
|
|
207
212
|
|
|
208
213
|
// The panel needs a read/write channel, so these routes exist even while the
|
|
209
214
|
// plugin is switched off: that is how it gets switched back on.
|
|
210
|
-
registerPanelRoutes(ctx, config, stats, archive)
|
|
215
|
+
registerPanelRoutes(ctx, config, stats, archive, pressure)
|
|
211
216
|
}
|
|
212
217
|
|
|
213
218
|
/** Route prefix owned by this plugin. */
|
|
@@ -219,7 +224,7 @@ const API_PREFIX = '/compact-arsenal/api'
|
|
|
219
224
|
* @param {any} config - the config this instance mounted with.
|
|
220
225
|
* @param {{ reasoning: number, tools: number, chars: number }} stats - live counters.
|
|
221
226
|
*/
|
|
222
|
-
function registerPanelRoutes(ctx, config, stats, archive) {
|
|
227
|
+
function registerPanelRoutes(ctx, config, stats, archive, pressure) {
|
|
223
228
|
const webServer = ctx.get('webServer')
|
|
224
229
|
const configEditor = ctx.get('configEditor')
|
|
225
230
|
if (webServer === undefined) return
|
|
@@ -240,6 +245,7 @@ function registerPanelRoutes(ctx, config, stats, archive) {
|
|
|
240
245
|
lastFailure,
|
|
241
246
|
archiveRoot: archive.root,
|
|
242
247
|
lastArchivePath: archive.lastPath,
|
|
248
|
+
pressure,
|
|
243
249
|
})
|
|
244
250
|
})
|
|
245
251
|
|
|
@@ -274,6 +280,7 @@ function registerPanelRoutes(ctx, config, stats, archive) {
|
|
|
274
280
|
lastFailure,
|
|
275
281
|
archiveRoot: archive.root,
|
|
276
282
|
lastArchivePath: archive.lastPath,
|
|
283
|
+
pressure,
|
|
277
284
|
})
|
|
278
285
|
})
|
|
279
286
|
}
|
|
@@ -455,8 +462,8 @@ function transcriptLine(event) {
|
|
|
455
462
|
* @param {{ agent: { id: string }, turn: number }} payload - pre-step payload.
|
|
456
463
|
* @param {Map<string, number>} lastProcessedTurn - per-session guard.
|
|
457
464
|
*/
|
|
458
|
-
function rewrite(ctx, config, payload, lastProcessedTurn, stats, archive) {
|
|
459
|
-
return rewriteAsync(ctx, config, payload, lastProcessedTurn, stats, archive)
|
|
465
|
+
function rewrite(ctx, config, payload, lastProcessedTurn, stats, archive, pressure) {
|
|
466
|
+
return rewriteAsync(ctx, config, payload, lastProcessedTurn, stats, archive, pressure)
|
|
460
467
|
}
|
|
461
468
|
|
|
462
469
|
/**
|
|
@@ -468,7 +475,7 @@ function rewrite(ctx, config, payload, lastProcessedTurn, stats, archive) {
|
|
|
468
475
|
* @param {{ reasoning: number, tools: number, chars: number }} stats - live counters.
|
|
469
476
|
* @returns {Promise<void>} resolves when every rewrite of this turn landed.
|
|
470
477
|
*/
|
|
471
|
-
async function rewriteAsync(ctx, config, payload, lastProcessedTurn, stats, archive) {
|
|
478
|
+
async function rewriteAsync(ctx, config, payload, lastProcessedTurn, stats, archive, pressure) {
|
|
472
479
|
if (!config.total.enabled && !config.reasoning.enabled && !config.tools.enabled) return
|
|
473
480
|
|
|
474
481
|
const session = ctx.sessions.get(payload.agent.id)
|
|
@@ -478,6 +485,12 @@ async function rewriteAsync(ctx, config, payload, lastProcessedTurn, stats, arch
|
|
|
478
485
|
|
|
479
486
|
lastProcessedTurn.set(session.id, payload.turn)
|
|
480
487
|
|
|
488
|
+
// Snapshot the inputs once per turn so the panel can explain a gate decision.
|
|
489
|
+
const snapshot = pressureSnapshot(ctx, session)
|
|
490
|
+
pressure.reported = snapshot.reported ?? null
|
|
491
|
+
pressure.estimated = snapshot.estimated
|
|
492
|
+
pressure.window = session.requestContext?.()?.contextWindow ?? null
|
|
493
|
+
|
|
481
494
|
if (config.total.enabled && underPressure(ctx, session, config.total.pressureRatio)) {
|
|
482
495
|
await totalPass(ctx, session, config.total, payload.turn, payload.signal, stats, archive)
|
|
483
496
|
}
|
|
@@ -716,9 +729,47 @@ function underPressure(ctx, session, ratio) {
|
|
|
716
729
|
if (typeof window !== 'number' || window <= 0) return false
|
|
717
730
|
const r = Number.isFinite(ratio) ? Math.min(1, Math.max(0, ratio)) : 0.75
|
|
718
731
|
if (r <= 0) return true
|
|
732
|
+
const used = currentPressureTokens(ctx, session)
|
|
733
|
+
return used >= window * r
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Tokens the next request is expected to cost, preferring the provider's own
|
|
738
|
+
* count over the heuristic estimate.
|
|
739
|
+
*
|
|
740
|
+
* The heuristic prices text at ~4 characters per token, which undercounts CJK
|
|
741
|
+
* and JSON by roughly 12%: a session measured at 0.519 of its window can look
|
|
742
|
+
* like 0.454 and never trip the gate. `contextPressure()` carries the prompt
|
|
743
|
+
* size the provider last reported, so that figure wins whenever it exists.
|
|
744
|
+
*
|
|
745
|
+
* @param {any} ctx - plugin context.
|
|
746
|
+
* @param {any} session - live session.
|
|
747
|
+
* @returns {{ used: number, reported: number | undefined, estimated: number }} the pressure inputs.
|
|
748
|
+
*/
|
|
749
|
+
function pressureSnapshot(ctx, session) {
|
|
750
|
+
let reported
|
|
751
|
+
try {
|
|
752
|
+
const pressure = ctx.tokenMeter?.contextPressure?.(session)
|
|
753
|
+
if (pressure && typeof pressure === 'object') {
|
|
754
|
+
const value = pressure.projectedTokens ?? pressure.pressureTokens
|
|
755
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0) reported = value
|
|
756
|
+
}
|
|
757
|
+
} catch {
|
|
758
|
+
// A missing projection only means "fall back to the estimate".
|
|
759
|
+
}
|
|
719
760
|
const measured = ctx.tokenMeter?.measure?.(session)
|
|
720
|
-
const
|
|
721
|
-
return
|
|
761
|
+
const estimated = typeof measured?.totalTokens === 'number' ? measured.totalTokens : 0
|
|
762
|
+
return { used: reported ?? estimated, reported, estimated }
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
/**
|
|
766
|
+
* Just the token count the gate compares against the window.
|
|
767
|
+
* @param {any} ctx - plugin context.
|
|
768
|
+
* @param {any} session - live session.
|
|
769
|
+
* @returns {number} pressure tokens.
|
|
770
|
+
*/
|
|
771
|
+
function currentPressureTokens(ctx, session) {
|
|
772
|
+
return pressureSnapshot(ctx, session).used
|
|
722
773
|
}
|
|
723
774
|
|
|
724
775
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-compact-arsenal",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.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",
|