@a9i5k4/dsh-auto-memory 2.2.7 → 2.3.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/lib/index.js CHANGED
@@ -1750,6 +1750,23 @@ class MemoryEngine {
1750
1750
  return rec && Date.now() - (Number(rec.at) || 0) < 600000 ? rec : null
1751
1751
  }
1752
1752
 
1753
+ /** 2.3.1:解析会话日志文件路径 —— DSH 0.1.5 起会话格式迁移到 `session.v3.jsonl.zstd`
1754
+ * (旧 `session.jsonl.zstd` 保留但停止更新);按 mtime 取最新存在的文件,兼容两代与未来命名。 */
1755
+ async resolveSessionFile(dir) {
1756
+ try {
1757
+ const cands = ['session.v3.jsonl.zstd', 'session.jsonl.zstd', 'session.v3.jsonl', 'session.jsonl']
1758
+ let best = null
1759
+ for (const name of cands) {
1760
+ const p = path.join(dir, name)
1761
+ try {
1762
+ const st = await stat(p)
1763
+ if (st.isFile() && (!best || st.mtimeMs > best.mtimeMs)) best = { path: p, mtimeMs: st.mtimeMs }
1764
+ } catch (e) {}
1765
+ }
1766
+ return best ? best.path : ''
1767
+ } catch (e) { return '' }
1768
+ }
1769
+
1753
1770
  /** 从会话日志(磁盘)推导窗口与模型:不需要 agent,切换会话即可得到正确的模型名与窗口。 */
1754
1771
  async waterWindowForSession(sid) {
1755
1772
  const empty = { window: 0, source: '', model: '', provider: '', contextWindow: 0 }
@@ -1761,7 +1778,8 @@ class MemoryEngine {
1761
1778
  try {
1762
1779
  const dir = await this.locateSessionDir(String(sid))
1763
1780
  if (dir) {
1764
- const raw = await readFile(path.join(dir, 'session.jsonl.zstd')).catch(() => null)
1781
+ const sessionFile = await this.resolveSessionFile(dir)
1782
+ const raw = sessionFile ? await readFile(sessionFile).catch(() => null) : null
1765
1783
  const events = raw ? decodeZstdFrames(raw).split('\n').map((l) => { try { return JSON.parse(l) } catch (e) { return null } }).filter(Boolean) : []
1766
1784
  const info = findSessionModelPre(events)
1767
1785
  const wi = await this.resolveWaterWindow(info.provider, info.model)
@@ -2119,10 +2137,8 @@ class MemoryEngine {
2119
2137
  const sessionsDir = path.join(dshHome(), 'sessions')
2120
2138
  let file = null
2121
2139
  for (const w of await readdir(sessionsDir).catch(() => [])) {
2122
- for (const cand of [path.join(sessionsDir, w, sid, 'session.jsonl.zstd'), path.join(sessionsDir, w, sid, 'session.jsonl')]) {
2123
- try { const st = await stat(cand); if (st.isFile()) { file = cand; break } } catch (e) {}
2124
- }
2125
- if (file) break
2140
+ const resolved = await this.resolveSessionFile(path.join(sessionsDir, w, sid))
2141
+ if (resolved) { file = resolved; break }
2126
2142
  }
2127
2143
  if (!file) return { sessionId: sid }
2128
2144
  const buf = await readFile(file)
@@ -3229,7 +3245,7 @@ class MemoryEngine {
3229
3245
  pending.text,
3230
3246
  '请在本轮回复开头,以「昨日反思 · ' + pending.date + '」小节向用户呈现前一天的工作反思与要点:成果回顾、值得注意的教训或改进、今天可延续的要点。',
3231
3247
  '要求:' + styleText,
3232
- '生成后调用 memory_reflect(date="' + pending.date + '", text=完整反思内容)保存,之后该提示不再出现。',
3248
+ '生成后**必须**调用 memory_reflect(date="' + pending.date + '", text=完整反思内容)保存落盘 —— 只呈现不落盘会导致该提示在后续轮次重复出现。',
3233
3249
  ].join('\n'))
3234
3250
  }
3235
3251
 
@@ -4336,6 +4352,8 @@ class MemoryEngine {
4336
4352
 
4337
4353
  /** 调用 DSH subagent 发散/提炼一段文本(90s 超时,结果取 text 块;parent 用最近 agent)。 */
4338
4354
  async runSubagent(text, label, agent, timeoutMs) {
4355
+ // 2.2.7:插件 context 已卸载(重启/禁用中)→ 不再 spawn,避免向失效 context 注册 effect
4356
+ if (this._disposed) return ''
4339
4357
  const subagents = this._subagents
4340
4358
  if (!subagents) return ''
4341
4359
  // 配置性错误熔断(问题③):UNKNOWN_MODEL 等错误不会自愈,熔断期内直接返回空,不再反复 spawn(卡顿根因)
@@ -4422,6 +4440,11 @@ class MemoryEngine {
4422
4440
  return blocks.filter((b) => b && b.type === 'text').map((b) => b.text).join('').trim()
4423
4441
  } catch (e) {
4424
4442
  const em = e && e.message ? e.message : String(e)
4443
+ // 2.2.7:context 已卸载导致的 effect 创建失败是重启/卸载竞态,不是功能缺陷 —— 只记一次 diag,不刷屏
4444
+ if (/inactive context/i.test(em)) {
4445
+ if (!this._inactiveWarned) { this._inactiveWarned = true; diag('subagent ' + label + ' skipped: plugin context inactive (宿主已卸载,重启后自动恢复)') }
4446
+ return ''
4447
+ }
4425
4448
  console.error('[dsh-auto-memory] ' + label + ' failed', em)
4426
4449
  diag('subagent ' + label + ' failed: ' + em)
4427
4450
  // 配置性错误熔断(问题③):UNKNOWN_MODEL / 无配置模型 / 供应商不含该模型——重试无意义,30 分钟内不再 spawn
@@ -4455,13 +4478,30 @@ class MemoryEngine {
4455
4478
  const sid = session && (session.id || (header && header.id)) ? String(session.id || header.id) : ''
4456
4479
  if (!sid) return { ok: false, reason: 'no-id' }
4457
4480
  if (!header || header.origin !== 'subagent') return { ok: false, reason: 'not-subagent' }
4481
+ // 2.3.0:Windows 下 DSH 常持有会话文件句柄(子代理刚结束时仍在写)→ rename 抛 EPERM。
4482
+ // 同一 sid 失败后冷却 30 分钟不再尝试(避免刷屏),占用释放后由每日 sweep 兜底回收。
4483
+ const cool = this._gcCooldown || (this._gcCooldown = new Map())
4484
+ const coolUntil = cool.get(sid) || 0
4485
+ if (Date.now() < coolUntil) return { ok: false, reason: 'cooldown' }
4458
4486
  const dir = await this.locateSessionDir(sid, header)
4459
4487
  if (!dir) return { ok: false, reason: 'not-located' }
4460
4488
  const wsDir = path.basename(path.dirname(dir))
4461
4489
  const backupRoot = path.join(dshHome(), 'subagent-gc-backup')
4462
4490
  const dest = path.join(backupRoot, wsDir, sid)
4463
4491
  await mkdir(path.dirname(dest), { recursive: true })
4464
- await rename(dir, dest)
4492
+ let renamed = false
4493
+ let lastErr = null
4494
+ for (const delay of [0, 300, 1000]) {
4495
+ if (delay) await new Promise((r) => setTimeout(r, delay))
4496
+ try { await rename(dir, dest); renamed = true; break } catch (e) { lastErr = e }
4497
+ }
4498
+ if (!renamed) {
4499
+ cool.set(sid, Date.now() + 30 * 60 * 1000)
4500
+ // 首次失败记 diag,后续静默(同一 sid 冷却期内不再刷屏)
4501
+ const warned = this._gcWarned || (this._gcWarned = new Set())
4502
+ if (!warned.has(sid)) { warned.add(sid); diag('subagent gc deferred (file locked by host): ' + sid) }
4503
+ return { ok: false, reason: 'locked', sid }
4504
+ }
4465
4505
  // 投影缓存同步移走,避免孤儿条目长期占用索引
4466
4506
  try {
4467
4507
  const cacheFile = path.join(dshHome(), 'storages', 'session_projcache', 'sessions', sid)
@@ -4511,9 +4551,11 @@ class MemoryEngine {
4511
4551
  const keepDays = Math.max(0, Number(this.config.subagentGcKeepDays) || 0)
4512
4552
  const scan = await scanPluginSubagentSessions({ sessionsRoot, labelPrefix: PLUGIN_LABEL_PREFIX, keepMs: 0 })
4513
4553
  const now = Date.now()
4514
- // 无论保留期多长,最近 10 分钟的一律不动(可能与正在运行的子代理竞争)
4515
- const floorMs = Math.max(keepDays * 24 * 3600 * 1000, 10 * 60 * 1000)
4516
- const eligible = scan.candidates.filter((c) => now - c.mtimeMs >= floorMs)
4554
+ // 无论保留期多长,最近 60 分钟的一律不动(2.3.0:Windows 下宿主可能长期持有会话文件句柄,
4555
+ // 10 分钟窗口不足以判断"已不再写入";占用中的目录 rename 必失败)
4556
+ const floorMs = Math.max(keepDays * 24 * 3600 * 1000, 60 * 60 * 1000)
4557
+ const cool = this._gcCooldown || (this._gcCooldown = new Map())
4558
+ const eligible = scan.candidates.filter((c) => now - c.mtimeMs >= floorMs && now >= (cool.get(c.sid) || 0))
4517
4559
  if (!eligible.length) return { ok: true, scanned: scan.scanned, recycled: 0 }
4518
4560
  const res = await recycleSessions({
4519
4561
  candidates: eligible,
@@ -4522,6 +4564,8 @@ class MemoryEngine {
4522
4564
  apply: true,
4523
4565
  })
4524
4566
  diag('subagent gc sweep: recycled=' + res.moved.length + ' failed=' + res.failed.length + ' bytes=' + res.bytes)
4567
+ // 2.3.0:被占用的目录进入 30 分钟冷却,避免每日巡检重复无效重试
4568
+ for (const f of res.failed || []) { if (f && f.sid) cool.set(String(f.sid), Date.now() + 30 * 60 * 1000) }
4525
4569
  return { ok: true, scanned: scan.scanned, recycled: res.moved.length, failed: res.failed.length, bytes: res.bytes }
4526
4570
  } catch (e) {
4527
4571
  diag('subagent gc sweep failed: ' + (e && e.message ? e.message : String(e)))
@@ -6281,6 +6325,11 @@ export function apply(ctx, config) {
6281
6325
  const sessionQuery = ctx.get('sessionQuery')
6282
6326
  engine._sessionQuery = sessionQuery
6283
6327
  engine._subagents = ctx.get('subagents') || undefined
6328
+ // 2.2.7 修复:宿主 context 卸载后,残留的定时器/面板请求仍会调 runSubagent,
6329
+ // DSH 0.1.2 的 in-process 子代理会向已失效 context 注册 effect → "cannot create effect on inactive context" 刷屏。
6330
+ // 记 dispose 标记,runSubagent 入口直接短路。
6331
+ engine._disposed = false
6332
+ try { ctx.on('dispose', () => { engine._disposed = true }) } catch (eDispose) {}
6284
6333
  // 时间检测用的服务(软获取,缺失时定时兜底自动降级)
6285
6334
  engine._sessionsSvc = ctx.get('sessions') || undefined
6286
6335
  engine._agentSvc = ctx.get('agent') || undefined
@@ -167,9 +167,18 @@ export async function scanPluginSubagentSessions(opts = {}) {
167
167
  out.scanned++
168
168
  const sid = ent.name
169
169
  const dir = path.join(sessionsRoot, wsDir, sid)
170
- const file = path.join(dir, 'session.jsonl.zstd')
170
+ // 2.3.1:DSH 0.1.5 起会话格式迁移到 session.v3.jsonl.zstd(旧名前缀保留但停止更新);
171
+ // 按 mtime 取最新存在的文件,兼容两代命名。
172
+ let file = ''
171
173
  let st = null
172
- try { st = await stat(file) } catch (e) { bump('no-file'); continue }
174
+ for (const name of ['session.v3.jsonl.zstd', 'session.jsonl.zstd']) {
175
+ const cand = path.join(dir, name)
176
+ try {
177
+ const s = await stat(cand)
178
+ if (s.isFile() && (!st || s.mtimeMs > st.mtimeMs)) { st = s; file = cand }
179
+ } catch (e) {}
180
+ }
181
+ if (!st) { bump('no-file'); continue }
173
182
  let text = ''
174
183
  try { text = decodeZstdFrames(await readFile(file)) } catch (e) { bump('decode-error'); continue }
175
184
  const head = parseSessionHead(text)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@a9i5k4/dsh-auto-memory",
3
3
  "description": "Proactive associative memory for DSH: zero-prompt recall injected before the model speaks, three-layer auto-consolidation, skill crystallization, and Astra-style context management - handoff ledgers, PLAN whiteboard, water-level sensing. Local-first, model-agnostic, zero deps. 主动联想记忆+Astra 式上下文管理:自动唤回/自动沉淀/技能固化/交接账本与白板跨窗口续命/水位感知。",
4
- "version": "2.2.7",
4
+ "version": "2.3.1",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {