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 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 total = measured?.totalTokens ?? 0
721
- return total >= window * r
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.0",
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",