@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 +59 -10
- package/lib/subagent-gc.js +11 -2
- package/package.json +1 -1
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
|
|
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
|
-
|
|
2123
|
-
|
|
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
|
-
'
|
|
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
|
-
|
|
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
|
-
// 无论保留期多长,最近
|
|
4515
|
-
|
|
4516
|
-
const
|
|
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
|
package/lib/subagent-gc.js
CHANGED
|
@@ -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
|
-
|
|
170
|
+
// 2.3.1:DSH 0.1.5 起会话格式迁移到 session.v3.jsonl.zstd(旧名前缀保留但停止更新);
|
|
171
|
+
// 按 mtime 取最新存在的文件,兼容两代命名。
|
|
172
|
+
let file = ''
|
|
171
173
|
let st = null
|
|
172
|
-
|
|
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.
|
|
4
|
+
"version": "2.3.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|