@a9i5k4/dsh-auto-memory 2.3.1 → 2.4.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.
@@ -29,6 +29,10 @@ import { fuseD6Pre } from './semantic-js.js'
29
29
  import { buildSourceCatalog, loadCorpusSnapshot, CorpusRegistry, canonicalize } from './m4-corpus.js'
30
30
 
31
31
  const MAX_DROPS_RING = 64
32
+ /** 同一 drop 原因的日志限流窗口(ms):窗口内只写首条,其余计数。防会话回放刷屏(2026-09-10)。 */
33
+ const DROP_LOG_WINDOW_MS = 30000
34
+ const dropLoggedAt = new Map()
35
+ const dropLoggedSkip = new Map()
32
36
 
33
37
  /** M7.5 诊断:ctx-host drop/skip 直写 harness diagnose 日志(此前 try{diag()} 静默吞 ReferenceError)。 */
34
38
  function diagCtx(msg) {
@@ -163,10 +167,24 @@ export function createContextHost(opts = {}) {
163
167
  if (!st) { st = { lastPushedContextVersion: -1, disposed: false }; states.set(runtime, st) }
164
168
  return st
165
169
  }
166
- function pushDrop(reason, contextVersion) {
170
+ function pushDrop(reason, contextVersion, runtimeKey) {
167
171
  volatileDrops.push({ at: Date.now(), reason, contextVersion })
168
172
  if (volatileDrops.length > MAX_DROPS_RING) volatileDrops.shift()
169
- diagCtx('ctx-host drop: ' + reason + ' cv=' + contextVersion)
173
+ // 回放风暴限流(2026-09-10):DSH 载入/观测一个历史会话时会对每一段历史都调一次 accept,
174
+ // 而该 runtime 从未 capturePaths → 同一原因一次性刷出上百行(实测单次回放 288 行,占诊断日志近半)。
175
+ // 同因日志按时间窗限流:窗口内只写首条,首条附 runtime key(定位是哪个会话),其余仅计数。
176
+ const _k = String(reason)
177
+ const _now = Date.now()
178
+ if (_now - (dropLoggedAt.get(_k) || 0) < DROP_LOG_WINDOW_MS) {
179
+ dropLoggedSkip.set(_k, (dropLoggedSkip.get(_k) || 0) + 1)
180
+ return
181
+ }
182
+ dropLoggedAt.set(_k, _now)
183
+ const _skip = dropLoggedSkip.get(_k) || 0
184
+ dropLoggedSkip.set(_k, 0)
185
+ diagCtx('ctx-host drop: ' + reason + ' cv=' + contextVersion
186
+ + (runtimeKey ? ' key=' + String(runtimeKey).slice(0, 48) : '')
187
+ + (_skip ? ' (+' + _skip + ' suppressed in ' + (DROP_LOG_WINDOW_MS / 1000) + 's)' : ''))
170
188
  }
171
189
  function capturePaths(runtimeKey, p) {
172
190
  pathsByKey.set(String(runtimeKey || ''), {
@@ -290,7 +308,7 @@ export function createContextHost(opts = {}) {
290
308
  // M7-8 live-parity 诊断:记录最近 Segment 的 runtime key/sessionRef(最小投影)
291
309
  lastSegmentRuntimeKey = String(runtime.key || '')
292
310
  lastSegmentSessionRef = String(runtime.sessionId || '').slice(0, 24)
293
- if (!paths) { pushDrop('no-paths-captured', seg.contextVersion); return }
311
+ if (!paths) { pushDrop('no-paths-captured', seg.contextVersion, runtime.key); return }
294
312
  const corpusSnap = loadCorpus(paths)
295
313
  // ---- cite / correction(user+assistant 可见文本;precision-first,需完整 token+provenance)----
296
314
  if ((seg.kind === 'user' || seg.kind === 'assistant') && seg.text && corpusSnap && corpusSnap.records.length) {
@@ -331,7 +349,7 @@ export function createContextHost(opts = {}) {
331
349
  void Promise.all([readyPromise, c2RankPromise]).then(([readyRes, rank]) => {
332
350
  if (!(readyRes && readyRes.ready)) {
333
351
  stats.pushesRejected++
334
- pushDrop('index-not-ready:' + ((readyRes && readyRes.reason) || 'unknown'), runtime.contextVersion)
352
+ pushDrop('index-not-ready:' + ((readyRes && readyRes.reason) || 'unknown'), runtime.contextVersion, runtime.key)
335
353
  return
336
354
  }
337
355
  if (st.disposed || runtime.disposed) return