@falling-ts/dsh-force-compact 0.2.4 → 0.2.6

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/index.js CHANGED
@@ -56,6 +56,7 @@ void thinkingDisabled
56
56
  import { registerCommand } from './src/hooks/command.js'
57
57
  import { handleAgentStatus } from './src/hooks/idle.js'
58
58
  import { registerLlmStreamHook } from './src/hooks/wire-rewrite.js'
59
+ import { publishWorkingOnStart } from './src/core/ui-signal.js'
59
60
  import { guardFn, installCrashNet } from './src/core/crashnet.js'
60
61
 
61
62
  /** @type {string} the function plugin's display name. */
@@ -373,6 +374,19 @@ const __applyInner = (ctx) => {
373
374
  maybeRegisterSettingsNamespace()
374
375
  maybeRegisterCommand()
375
376
  maybeInstallWireRewrite()
377
+ // CONVERSATION-START Live-UI FORCED OVERRIDE (self-healing): this seam is
378
+ // the FIRST model request of a conversation step, so force-clear any stale
379
+ // pinned bracket-form text ([强制压缩中>>>] / [压缩完成!] residue left by an
380
+ // interrupted or failed compaction) with a fresh random working pair
381
+ // (isImportant=true). Without this the non-important guard inside
382
+ // publishUiStatus would refuse to overwrite a `[`-prefixed text and the
383
+ // badge could stay frozen on a stale banner for the whole session. Fire-
384
+ // and-forget, like the llm/stream watermark: publishWorkingOnStart swallows
385
+ // its own rejections internally and must never block the request chain.
386
+ // Mid-request watermarking stays NON-important (publishRandomWorking at the
387
+ // llm/stream hook) so a live compressing/done banner during an ACTUAL
388
+ // compaction still survives; only the START of a conversation clears it.
389
+ void publishWorkingOnStart(ctx)
376
390
  return await __agentRequestListenerBody(ctx, payload, next)
377
391
  } catch (error) {
378
392
  const message = error instanceof Error ? error.message : String(error)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falling-ts/dsh-force-compact",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "DSH Cordis plugin: hooks the core model-request seam (agent/pre-step + agent/request) to force-compact a session's context and disable thinking per the \"强制压缩配置\" settings namespace (disableThinking, autoThresholdTokens, retainLatestTokens, turnEndForceCompactionEnabled). When the threshold fires (or /force-compact queues while busy), the latest `retainLatestTokens` of the conversation's surface tokens are KEPT VERBATIM and everything before that cutoff is COMPACTED INTO A SINGLE SUMMARY NODE in one LLM call (original span entries become shadowed/skipped). Also compacts at each turn/end and at each session/flush durability checkpoint. Host half is a pure listener; a web client half registers a settings.section (强制压缩 / Force Compact) that reads and writes the same settings namespace.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -82,7 +82,7 @@ export const WORKING_TEXTS = Object.freeze([
82
82
  '正在驯服混沌...',
83
83
  '正在召唤赛博大脑...',
84
84
  '正在翻阅《天机》...',
85
- 'CPU 正在冒烟...',
85
+ 'GPU 正在冒烟...',
86
86
  '正在跟熵值搏斗...',
87
87
  '正在画饼给你吃...',
88
88
  '正在偷渡灵感...',
@@ -227,6 +227,32 @@ export async function publishRandomWorking(ctx) {
227
227
  await publishUiStatus(ctx, randomWorkingPair())
228
228
  }
229
229
 
230
+ /**
231
+ * The conversation-START forced override: publish a fresh random working pair
232
+ * with `isImportant=true`, so it UNCONDITIONALLY overwrites whatever is
233
+ * currently displayed — including a stale pinned bracket-form text such as
234
+ * `[强制压缩中>>>]` left behind by an interrupted/failed compaction (the
235
+ * non-important guard inside {@link publishUiStatus} refuses to overwrite a
236
+ * `[`-prefixed text, so such residue would otherwise stick on the badge for
237
+ * the whole session, forever showing a frozen compressing/done banner instead
238
+ * of a live working label).
239
+ *
240
+ * Call ONCE at the START of a conversation step (the `agent/request` seam —
241
+ * see index.js): the first model request of a new turn force-clears any
242
+ * bracket residue so the badge immediately returns to a normal working look.
243
+ * Only the START uses the forced form; the mid-stream watermark keeps
244
+ * publishing NON-importantly via {@link publishRandomWorking} (called from the
245
+ * `llm/stream` hook) so a live `[强制压缩中>>>]` during an ACTUAL compaction
246
+ * still survives (the guard stays effective while something is really
247
+ * pressing). Clean, self-healing: a stale `[` never outlives the next
248
+ * conversation start.
249
+ * @param {import('@deepseek-ai/cordis').Context} ctx
250
+ * @returns {Promise<void>}
251
+ */
252
+ export async function publishWorkingOnStart(ctx) {
253
+ await publishUiStatus(ctx, randomWorkingPair(), true)
254
+ }
255
+
230
256
  /**
231
257
  * Publish the pinned RED "[强制压缩中>>>]" status. Call BEFORE a `compactNow` /
232
258
  * `compactRegion` invocation. Passes `isImportant=true` so the pinned
@@ -618,15 +618,29 @@ async function __forceCompactIfNeededBody(ctx, agent, signal, mode) {
618
618
  )
619
619
  }
620
620
 
621
- // At or above the threshold: do NOT request the model. Retain the latest
622
- // `retainLatestTokens` of the surface VERBATIM, and send everything before
623
- // that cutoff (the head) as ONE BATCH to the LLM summarizer. The loop retries
624
- // the step against the shrunken context (retained tail unchanged).
625
- ctx.logger.info(
626
- `[force-compact] ${session.id}: context ~${total} tokens >= threshold ${settings.autoThresholdTokens}; `
627
- + `rejecting the model request and compacting the head while retaining the latest ~${settings.retainLatestTokens} tokens`,
621
+ // SHORT-CIRCUIT: when the whole current surface window does not exceed the
622
+ // retention budget, there is no head to compact (the tail-retaining selector
623
+ // would walk the entire window and return null). The threshold was tripped
624
+ // by the provider-usage baseline rather than by real surface growth, so an
625
+ // attempted compaction is a guaranteed no-op — skip it and let the request
626
+ // proceed without pretending to compact.
627
+ if (windowSumObserved > 0 && Number.isFinite(windowSumObserved) && windowSumObserved <= settings.retainLatestTokens) {
628
+ ctx.logger.debug(
629
+ `[force-compact] ${session.id}: threshold ${settings.autoThresholdTokens} tripped on a surface window (~${Math.round(windowSumObserved)} tokens) that does not exceed retainLatestTokens (~${settings.retainLatestTokens}) — nothing above the retention floor to compact; letting the request proceed`,
630
+ )
631
+ return false
632
+ }
633
+
634
+ // At or above the threshold: attempt a retained-tail compaction (the request
635
+ // itself always proceeds; the plugin never rejects the model call).
636
+ ctx.logger.debug(
637
+ `[force-compact] ${session.id}: context ~${total} tokens >= threshold ${settings.autoThresholdTokens} — attempting a retained-tail compaction (success/failure is reported by the backend below; the request itself proceeds regardless)`,
628
638
  )
629
- const committed = await compactRetainingLatest(ctx, agent, signal, mode, measurement)
639
+ // NOTE: threshold path has NO originating slash-command, so the 5th argument
640
+ // (sourceCommandId) must be omitted — passing something else here (e.g. the
641
+ // `measurement` snapshot, as an earlier revision did) would leak a non-string
642
+ // into the compaction/* events' sourceCommandId field.
643
+ const committed = await compactRetainingLatest(ctx, agent, signal, mode)
630
644
  if (!committed) {
631
645
  // BLANK OUTCOME — nothing shrank, so the NEXT step re-attempts at the same
632
646
  // total. That is intentional ("先压缩再说"): a blank result never wedges the