@a9i5k4/dsh-auto-memory 0.1.35 → 0.1.36
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/README.md +2 -2
- package/README.zh-CN.md +2 -2
- package/lib/index.js +148 -27
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<a href="docs/screenshots/promo/promo-
|
|
8
|
+
<a href="docs/screenshots/promo/promo-0-banner-v2.png"><img width="820" alt="dsh-auto-memory hero: she remembers, unbidden" src="docs/screenshots/promo/promo-0-banner-v2.png"></a>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<a href="docs/screenshots/promo/promo-
|
|
12
|
+
<a href="docs/screenshots/promo/promo-0-banner-v2.png"><img width="130" alt="hero" src="docs/screenshots/promo/promo-0-banner-v2.png"></a>
|
|
13
13
|
<a href="docs/screenshots/promo/promo-2-tour.png"><img width="130" alt="welcome tour" src="docs/screenshots/promo/promo-2-tour.png"></a>
|
|
14
14
|
<a href="docs/screenshots/promo/promo-3-recall.png"><img width="130" alt="recall & crystallization" src="docs/screenshots/promo/promo-3-recall.png"></a>
|
|
15
15
|
<a href="docs/screenshots/promo/promo-4-unattended.png"><img width="130" alt="unattended mode" src="docs/screenshots/promo/promo-4-unattended.png"></a>
|
package/README.zh-CN.md
CHANGED
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
</p>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
<a href="docs/screenshots/promo/promo-
|
|
8
|
+
<a href="docs/screenshots/promo/promo-0-banner-v2.png"><img width="820" alt="dsh-auto-memory 主视觉:不用吩咐,她自己记得" src="docs/screenshots/promo/promo-0-banner-v2.png"></a>
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<a href="docs/screenshots/promo/promo-
|
|
12
|
+
<a href="docs/screenshots/promo/promo-0-banner-v2.png"><img width="130" alt="主视觉" src="docs/screenshots/promo/promo-0-banner-v2.png"></a>
|
|
13
13
|
<a href="docs/screenshots/promo/promo-2-tour.png"><img width="130" alt="欢迎向导" src="docs/screenshots/promo/promo-2-tour.png"></a>
|
|
14
14
|
<a href="docs/screenshots/promo/promo-3-recall.png"><img width="130" alt="唤起与固化" src="docs/screenshots/promo/promo-3-recall.png"></a>
|
|
15
15
|
<a href="docs/screenshots/promo/promo-4-unattended.png"><img width="130" alt="无人值守" src="docs/screenshots/promo/promo-4-unattended.png"></a>
|
package/lib/index.js
CHANGED
|
@@ -668,6 +668,10 @@ class MemoryEngine {
|
|
|
668
668
|
this._autoCallDate = ''
|
|
669
669
|
this._autoCallCount = 0
|
|
670
670
|
this._lastConsolidateStartedAt = 0
|
|
671
|
+
this._globalLastActiveAt = 0 // 全局活动时间戳:任意会话/工作区任一活动即更新(暂离检测统揽全局,不按工作区)
|
|
672
|
+
this._ownSubagents = new WeakSet() // 本插件 spawn 的子代理(防套娃:其生命周期事件零处理;WeakSet 不阻止 GC)
|
|
673
|
+
this._subagentInflight = 0 // 子代理并发闸门(启动瞬间多路 spawn 叠加是卡顿根因之一)
|
|
674
|
+
this._subagentCircuit = null // 配置性错误熔断 { until, reason }(UNKNOWN_MODEL 等,重试无意义)
|
|
671
675
|
this._smartRecallFlight = undefined
|
|
672
676
|
this._budgets = undefined // 每日写入预算:用户级4000/项目级3000字/天(所有会话共享,跨天重置)
|
|
673
677
|
// M2: 进程级观察统计(无 owner 而无法归属 runtime 的事件只在这里留痕,绝不落入 default runtime)
|
|
@@ -1375,6 +1379,42 @@ class MemoryEngine {
|
|
|
1375
1379
|
}
|
|
1376
1380
|
}
|
|
1377
1381
|
|
|
1382
|
+
/** 全局层路径:每日总结/时段问候跨工作区统揽(不落在任何单一工作区)。 */
|
|
1383
|
+
globalPaths() {
|
|
1384
|
+
const userDir = this.userDirOf()
|
|
1385
|
+
return {
|
|
1386
|
+
userDir,
|
|
1387
|
+
summariesDir: path.join(userDir, 'summaries'),
|
|
1388
|
+
greetDir: path.join(userDir, 'greetings'),
|
|
1389
|
+
greetPath: path.join(userDir, 'greetings', `${this.memToday()}.json`),
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
/**
|
|
1394
|
+
* 全局日志扫描:聚合所有工作区指定日期的日志条目(统揽全局,而非单工作区)。
|
|
1395
|
+
* 返回 [{ ws, date, text, lines:[...] }],按工作区名排序,读文件失败的工作区跳过。
|
|
1396
|
+
*/
|
|
1397
|
+
async listAllWorkspaceLogs(date) {
|
|
1398
|
+
const out = []
|
|
1399
|
+
try {
|
|
1400
|
+
if (!this.configLoaded) { try { await this.loadConfig() } catch (e) {} }
|
|
1401
|
+
const root = this.expandUserPath(this.config.memoryRoot) || path.join(dshHome(), 'memory', 'workspaces')
|
|
1402
|
+
let entries = []
|
|
1403
|
+
try { entries = await readdir(root, { withFileTypes: true }) } catch (e) { return out }
|
|
1404
|
+
for (const ent of entries) {
|
|
1405
|
+
if (!ent.isDirectory()) continue
|
|
1406
|
+
const ws = ent.name
|
|
1407
|
+
const logFile = path.join(root, ws, `${date}.md`)
|
|
1408
|
+
let text = ''
|
|
1409
|
+
try { text = await this.readTextSafe(logFile) } catch (e) { continue }
|
|
1410
|
+
if (!text) continue
|
|
1411
|
+
out.push({ ws, date, text, lines: text.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('- ')).map((l) => l.replace(/^- /, '')) })
|
|
1412
|
+
}
|
|
1413
|
+
} catch (e) {}
|
|
1414
|
+
out.sort((a, b) => String(a.ws).localeCompare(String(b.ws)))
|
|
1415
|
+
return out
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1378
1418
|
// ---------- 更新检查(registry 自动检查 + profile 探测) ----------
|
|
1379
1419
|
/** 找 dsh web profile 目录(含 dsh-auto-memory 依赖/bundle 的那个)。 */
|
|
1380
1420
|
async findProfileDir() {
|
|
@@ -1655,6 +1695,15 @@ class MemoryEngine {
|
|
|
1655
1695
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
1656
1696
|
}
|
|
1657
1697
|
|
|
1698
|
+
/** 记忆日偏移:memToday() 基础上加减 N 天(供"昨天"日志回看)。 */
|
|
1699
|
+
memTodayOfOffset(days) {
|
|
1700
|
+
const s = this.memToday()
|
|
1701
|
+
const [y, m, dd] = s.split('-').map(Number)
|
|
1702
|
+
const d = new Date(y, m - 1, dd)
|
|
1703
|
+
d.setDate(d.getDate() + Number(days || 0))
|
|
1704
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1658
1707
|
/**
|
|
1659
1708
|
* 是否应处于无人值守模式(2026-08-26,roadmap「自动检测」)。
|
|
1660
1709
|
* 优先级:手动 unattendedMode=true → 恒 true;否则 unattendedAuto=true 且(命中非工作时间窗
|
|
@@ -1949,7 +1998,9 @@ class MemoryEngine {
|
|
|
1949
1998
|
}
|
|
1950
1999
|
// 暂离回来提示:距上次活动>1小时,要求 agent 在回复开头写欢迎语并提示打开记忆窗口
|
|
1951
2000
|
// ——无人值守模式下剥离(不注入社交性/行为性指令,避免无人值守任务浪费 token 在寒暄上)
|
|
1952
|
-
|
|
2001
|
+
let lastActiveGlobal = this._globalLastActiveAt || 0
|
|
2002
|
+
try { for (const rt of this.runtimes.values()) { const t = Number(rt.lastActiveAt) || 0; if (t > lastActiveGlobal) lastActiveGlobal = t } } catch (e) {}
|
|
2003
|
+
if (!this.isUnattendedNow() && lastActiveGlobal && Date.now() - lastActiveGlobal > 3600000) {
|
|
1953
2004
|
lines.push('\n' + L('snapshotWelcomeTitle'))
|
|
1954
2005
|
lines.push(L('snapshotWelcomeBody'))
|
|
1955
2006
|
}
|
|
@@ -2679,16 +2730,11 @@ class MemoryEngine {
|
|
|
2679
2730
|
|
|
2680
2731
|
/** 生活化总结:调用 DSH 的 AI(subagent)发散地总结某时段的工作(完成/收尾/烦恼/暂停的事)。 */
|
|
2681
2732
|
async summarizePeriod(period, agent, force) {
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
const
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
let cacheFile = ''
|
|
2688
|
-
try {
|
|
2689
|
-
const p = await this.resolvePaths(agent)
|
|
2690
|
-
cacheFile = path.join(p.projectDir, 'summaries', this.memToday() + '-' + period + '.json')
|
|
2691
|
-
} catch (e) {}
|
|
2733
|
+
// 全局统揽(2026-09-01):时段总结不再绑定单一工作区——聚合所有工作区的日志,
|
|
2734
|
+
// 源文件列表与缓存都走用户级目录(~/.dsh/memory/summaries/),换工作区总结内容与缓存不变。
|
|
2735
|
+
const today = this.memToday()
|
|
2736
|
+
const gp = this.globalPaths()
|
|
2737
|
+
const cacheFile = path.join(gp.summariesDir, today + '-' + period + '.json')
|
|
2692
2738
|
if (!force && cacheFile) {
|
|
2693
2739
|
try {
|
|
2694
2740
|
const raw = await this.readTextSafe(cacheFile)
|
|
@@ -2698,14 +2744,27 @@ class MemoryEngine {
|
|
|
2698
2744
|
}
|
|
2699
2745
|
} catch (e) {}
|
|
2700
2746
|
}
|
|
2701
|
-
|
|
2702
|
-
const
|
|
2703
|
-
|
|
2704
|
-
|
|
2747
|
+
// 候选条目:全局扫描所有工作区(当前时段用今天的日志,昨天用昨天的)
|
|
2748
|
+
const scanDate = (period === '昨天') ? (this.state.recentLogs[0] ? this.state.recentLogs[0].date : this.memTodayOfOffset(-1)) : today
|
|
2749
|
+
const wsLogs = await this.listAllWorkspaceLogs(scanDate)
|
|
2750
|
+
// 兼容:若全局扫描为空(如记忆根迁移中),降级到当前工作区旧路径
|
|
2751
|
+
let items = wsLogs.flatMap((w) => w.lines)
|
|
2752
|
+
let sourceFiles = wsLogs.map((w) => path.join(this.expandUserPath(this.config.memoryRoot) || path.join(dshHome(), 'memory', 'workspaces'), w.ws, `${w.date}.md`))
|
|
2753
|
+
if (!wsLogs.length) {
|
|
2754
|
+
try {
|
|
2755
|
+
const paths = await this.resolvePaths(agent)
|
|
2756
|
+
const legacy = (period === '昨天')
|
|
2757
|
+
? (this.state.recentLogs[0] ? [{ file: path.join(paths.projectDir, this.state.recentLogs[0].date + '.md'), text: this.state.recentLogs[0].text }] : [])
|
|
2758
|
+
: [{ file: paths.logPath, text: '' }]
|
|
2759
|
+
items = (period === '昨天') ? legacy.flatMap((x) => x.text.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('- ')).map((l) => l.replace(/^- /, ''))) : (this.periodSummary().groups || {})[period] || []
|
|
2760
|
+
sourceFiles = legacy.map((x) => x.file)
|
|
2761
|
+
} catch (e) {}
|
|
2762
|
+
}
|
|
2763
|
+
if (!items.length) return { summary: '', works: [], cached: true, generatedAt: Date.now() }
|
|
2705
2764
|
const prompt = [
|
|
2706
|
-
'
|
|
2707
|
-
|
|
2708
|
-
'
|
|
2765
|
+
'你是一个温暖、细腻的生活助理。请使用文件读取工具依次读取下面的绝对路径(跨多个工作区的记忆日志,请统揽全局),不要要求用户粘贴内容:',
|
|
2766
|
+
...sourceFiles.map((f, i) => (i + 1) + '. ' + f),
|
|
2767
|
+
'从这些文件中只筛选“' + period + '”时段的工作条目(昨天则读取整份昨天日志),共有约 ' + items.length + ' 条候选记录。',
|
|
2709
2768
|
'1. 写一段生活化总结(80-160字),像朋友聊天:概括完成的事、是否收尾、可能的烦恼(温和带过,看不出就跳过);不要列表、不要小标题、不要"总结:"前缀。',
|
|
2710
2769
|
'2. 把原始记录归纳成若干项“工作”(3-6项),每项给简短标题(8-16字),并列出细点(每项2-4条)。',
|
|
2711
2770
|
'输出格式(严格遵守,不要多余文字):',
|
|
@@ -2740,9 +2799,15 @@ class MemoryEngine {
|
|
|
2740
2799
|
// ---------- 时间检测(每 15s 心跳 tick):暂离状态 / _lastAgent 定时兜底 / 自动总结时间点 ----------
|
|
2741
2800
|
tickTime() {
|
|
2742
2801
|
try {
|
|
2743
|
-
// 1)
|
|
2802
|
+
// 1) 暂离检测(全局统揽):取所有 runtime 里最近一次活动时间(跨工作区),而非当前 runtime 的孤立时间戳。
|
|
2803
|
+
let lastActive = this._globalLastActiveAt || 0
|
|
2804
|
+
try { for (const rt of this.runtimes.values()) { const t = Number(rt.lastActiveAt) || 0; if (t > lastActive) lastActive = t } } catch (e) {}
|
|
2805
|
+
this._globalLastActiveAt = lastActive
|
|
2744
2806
|
const awayMs = Math.max(Number(this.config.awayMinutes) || 60, 1) * 60000
|
|
2745
|
-
|
|
2807
|
+
const away = !!(lastActive && Date.now() - lastActive > awayMs)
|
|
2808
|
+
// 广播到所有 runtime(面板/注入读各自 runtime 的 state.away,值一致=全局语义)
|
|
2809
|
+
try { for (const rt of this.runtimes.values()) rt.state.away = away } catch (e) {}
|
|
2810
|
+
this.state.away = away
|
|
2746
2811
|
// 2) _lastAgent 定时兜底(重启恢复会话不触发 session-start;pre-step 兜底之外的第二保险)
|
|
2747
2812
|
if (!this._lastAgent) this.restoreLastAgent()
|
|
2748
2813
|
// 3) 自动总结时间点:命中配置的 HH:MM 且当天未生成过 → 生成时段总结并标记展示
|
|
@@ -2811,8 +2876,9 @@ class MemoryEngine {
|
|
|
2811
2876
|
const hour = new Date().getHours()
|
|
2812
2877
|
const seg = hour < 6 ? 'morning' : hour < 9 ? 'morning' : hour < 12 ? 'forenoon' : hour < 14 ? 'noon' : hour < 18 ? 'afternoon' : 'evening'
|
|
2813
2878
|
const segLabel = { morning: '早上', forenoon: '上午', noon: '中午', afternoon: '下午', evening: '晚上' }[seg]
|
|
2814
|
-
|
|
2815
|
-
const
|
|
2879
|
+
// 全局统揽(2026-09-01):问候缓存迁用户级目录(跨工作区共享同一份),引用条目聚合全部工作区今日日志
|
|
2880
|
+
const gp = this.globalPaths()
|
|
2881
|
+
const greetFile = path.join(gp.greetDir, this.memToday() + '.json')
|
|
2816
2882
|
// 已有该时段缓存 → 直接返回
|
|
2817
2883
|
try {
|
|
2818
2884
|
const raw = await this.readTextSafe(greetFile)
|
|
@@ -2821,8 +2887,13 @@ class MemoryEngine {
|
|
|
2821
2887
|
if (j && j[seg]) return { greeting: j[seg], cached: true }
|
|
2822
2888
|
}
|
|
2823
2889
|
} catch (e) {}
|
|
2824
|
-
// 今日记录(
|
|
2825
|
-
const
|
|
2890
|
+
// 今日记录(供问候引用最近几条;聚合所有工作区,统揽全局)
|
|
2891
|
+
const wsLogs = await this.listAllWorkspaceLogs(this.memToday())
|
|
2892
|
+
let items = wsLogs.flatMap((w) => w.lines.map((l) => l.replace(/^- \d{2}:\d{2} /, '').replace(/^- /, ''))).slice(-6)
|
|
2893
|
+
if (!items.length) {
|
|
2894
|
+
// 兼容降级:全局扫描为空时回落当前工作区
|
|
2895
|
+
items = (this.state.logText || '').split('\n').map((l) => l.trim()).filter((l) => l.startsWith('- ')).map((l) => l.replace(/^- \d{2}:\d{2} /, '').replace(/^- /, '')).slice(-6)
|
|
2896
|
+
}
|
|
2826
2897
|
const prompt = [
|
|
2827
2898
|
'你是一个温暖的生活助理。现在是' + segLabel + ',请给用户写一句简短的拟人化问候(30-60字),像朋友一样:',
|
|
2828
2899
|
'- 按时间段自然问候(' + segLabel + '好/辛苦了之类),自然提起今天完成的主要工作(挑1-2件最重要的),可以带一句贴心提醒(如早点休息)',
|
|
@@ -2838,7 +2909,7 @@ class MemoryEngine {
|
|
|
2838
2909
|
let all = {}
|
|
2839
2910
|
try { const raw = await this.readTextSafe(greetFile); if (raw) { const j = JSON.parse(raw); if (j) all = j } } catch (e) {}
|
|
2840
2911
|
all[seg] = text
|
|
2841
|
-
try { await mkdir(
|
|
2912
|
+
try { await mkdir(gp.greetDir, { recursive: true }); await writeFile(greetFile, JSON.stringify(all, null, 2), 'utf8') } catch (e) {}
|
|
2842
2913
|
return { greeting: text, cached: false }
|
|
2843
2914
|
}
|
|
2844
2915
|
|
|
@@ -2846,6 +2917,18 @@ class MemoryEngine {
|
|
|
2846
2917
|
async runSubagent(text, label, agent, timeoutMs) {
|
|
2847
2918
|
const subagents = this._subagents
|
|
2848
2919
|
if (!subagents) return ''
|
|
2920
|
+
// 配置性错误熔断(问题③):UNKNOWN_MODEL 等错误不会自愈,熔断期内直接返回空,不再反复 spawn(卡顿根因)
|
|
2921
|
+
try {
|
|
2922
|
+
if (this._subagentCircuit && Date.now() < this._subagentCircuit.until) {
|
|
2923
|
+
diag('subagent ' + label + ' circuit-open: ' + this._subagentCircuit.reason)
|
|
2924
|
+
return ''
|
|
2925
|
+
}
|
|
2926
|
+
} catch (e) {}
|
|
2927
|
+
// 并发闸门(问题②):同时在飞的子代理 ≥3 时拒绝新 spawn(启动瞬间多路叠加导致卡顿)
|
|
2928
|
+
if (this._subagentInflight >= 3) {
|
|
2929
|
+
diag('subagent ' + label + ' skipped: inflight=' + this._subagentInflight)
|
|
2930
|
+
return ''
|
|
2931
|
+
}
|
|
2849
2932
|
// parent 必须是完整 agent 对象(captureDelegatedPolicyOverrides 读 parent.ctx/parent.session);路由调用用缓存的最近 agent
|
|
2850
2933
|
const parent = agent || this._lastAgent
|
|
2851
2934
|
if (!parent || !parent.session || !parent.ctx || typeof parent.ctx.get !== 'function') {
|
|
@@ -2865,6 +2948,7 @@ class MemoryEngine {
|
|
|
2865
2948
|
try {
|
|
2866
2949
|
// prompt 必须是 block 数组(createUserMessage 校验 content.some)
|
|
2867
2950
|
const subModel = String(this.config.subagentModel || '').trim()
|
|
2951
|
+
this._subagentInflight = (this._subagentInflight || 0) + 1
|
|
2868
2952
|
run = await subagents.start(providerName, {
|
|
2869
2953
|
label,
|
|
2870
2954
|
prompt: [{ type: 'text', text }],
|
|
@@ -2873,6 +2957,8 @@ class MemoryEngine {
|
|
|
2873
2957
|
// 设置页「总结/问候默认模型」:非空时覆盖子代理的模型选择(空=跟随路由默认)
|
|
2874
2958
|
...(subModel ? { agentOptions: { model: subModel } } : {}),
|
|
2875
2959
|
})
|
|
2960
|
+
// 套娃防护(问题②):登记本插件 spawn 的子代理,其生命周期事件在 pre-step/turn-stopping 零处理
|
|
2961
|
+
try { if (run && run.agent) this._ownSubagents.add(run.agent) } catch (eReg) {}
|
|
2876
2962
|
// UNKNOWN_MODEL 自保:子代理路由为两源拼接(provider 继承父会话 + 模型落宿主全局默认),
|
|
2877
2963
|
// 父 provider 目录可能不含该模型(如 opencode-go-free × deepseek-v4-flash);
|
|
2878
2964
|
// 此时回退用父会话抽屉当前模型(settings.yaml agent-default-model)重试一次。
|
|
@@ -2910,8 +2996,15 @@ class MemoryEngine {
|
|
|
2910
2996
|
const em = e && e.message ? e.message : String(e)
|
|
2911
2997
|
console.error('[dsh-auto-memory] ' + label + ' failed', em)
|
|
2912
2998
|
diag('subagent ' + label + ' failed: ' + em)
|
|
2999
|
+
// 配置性错误熔断(问题③):UNKNOWN_MODEL / 无配置模型 / 供应商不含该模型——重试无意义,30 分钟内不再 spawn
|
|
3000
|
+
// (匹配收紧:避免普通网络错误的 message 恰含 "provider" 字样被误熔断)
|
|
3001
|
+
if (/UNKNOWN_MODEL|has no configured model|no configured provider|provider.*(not|no).*(config|model)|cannot find package/i.test(em)) {
|
|
3002
|
+
this._subagentCircuit = { until: Date.now() + 30 * 60000, reason: em.slice(0, 120) }
|
|
3003
|
+
diag('subagent circuit OPEN for 30min: ' + em.slice(0, 120))
|
|
3004
|
+
}
|
|
2913
3005
|
return ''
|
|
2914
3006
|
} finally {
|
|
3007
|
+
this._subagentInflight = Math.max(0, (this._subagentInflight || 1) - 1)
|
|
2915
3008
|
clearTimeout(timer)
|
|
2916
3009
|
if (controller.signal.aborted && run && typeof run.dispose === 'function') {
|
|
2917
3010
|
try { await run.dispose() } catch (e2) {}
|
|
@@ -3077,11 +3170,21 @@ class MemoryEngine {
|
|
|
3077
3170
|
// 超时兜底:subagent 挂起(如会话收尾期)时 40s 后放弃并走重试队列,避免 _consolidating 永久占用导致后续轮次全部 skip busy
|
|
3078
3171
|
const text = await this.withTimeout(this.runSubagent(prompt, 'auto-memory-consolidate', agent), 40000, '')
|
|
3079
3172
|
if (!text) {
|
|
3173
|
+
// 配置性错误熔断中(问题③):UNKNOWN_MODEL 等不会自愈,不入队重试(重试=反复 spawn 卡顿)
|
|
3174
|
+
if (this._subagentCircuit && Date.now() < this._subagentCircuit.until) {
|
|
3175
|
+
diag('consolidate: circuit open (' + this._subagentCircuit.reason + ') — not queued')
|
|
3176
|
+
return
|
|
3177
|
+
}
|
|
3080
3178
|
diag('consolidate: subagent returned empty text (queued for retry)')
|
|
3081
|
-
// subagent 失败(返回空)
|
|
3082
|
-
|
|
3179
|
+
// subagent 失败(返回空):入重试队列,由后台轮询兜底重试;每条最多 3 次、存活 30 分钟(过期丢弃)
|
|
3180
|
+
const item = { turn, agent, tries: ((runtime._cqTries && runtime._cqTries.get(turn)) || 0) + 1, enqueuedAt: Date.now() }
|
|
3181
|
+
runtime._cqTries = runtime._cqTries || new Map()
|
|
3182
|
+
runtime._cqTries.set(turn, item.tries)
|
|
3183
|
+
if (item.tries <= 3 && runtime.pendingConsolidations.length < 5) runtime.pendingConsolidations.push(item)
|
|
3184
|
+
else { try { runtime._cqTries.delete(turn) } catch (e) {} }
|
|
3083
3185
|
return
|
|
3084
3186
|
}
|
|
3187
|
+
try { if (runtime._cqTries) runtime._cqTries.delete(turn) } catch (e) {}
|
|
3085
3188
|
if (text.includes('(无)')) return
|
|
3086
3189
|
const logPts = []
|
|
3087
3190
|
const notePts = []
|
|
@@ -3373,6 +3476,10 @@ class MemoryEngine {
|
|
|
3373
3476
|
// 时间检测:暂离状态 / 待展示的自动总结 / 相关配置
|
|
3374
3477
|
away: !!this.state.away,
|
|
3375
3478
|
awayMinutes: Math.max(Number(this.config.awayMinutes) || 60, 1),
|
|
3479
|
+
// 子代理熔断状态(问题③):配置性错误(如所选模型不在供应商目录)时置位,30 分钟后自动恢复
|
|
3480
|
+
subagentCircuit: (this._subagentCircuit && Date.now() < this._subagentCircuit.until)
|
|
3481
|
+
? { until: this._subagentCircuit.until, reason: this._subagentCircuit.reason }
|
|
3482
|
+
: null,
|
|
3376
3483
|
autoPopupEnabled: this.config.autoPopupEnabled !== false,
|
|
3377
3484
|
autoSummaryTimes: Array.isArray(this.config.autoSummaryTimes) ? this.config.autoSummaryTimes : [],
|
|
3378
3485
|
pendingSummary: this.state.pendingSummary || null,
|
|
@@ -4447,6 +4554,10 @@ export function apply(ctx, config) {
|
|
|
4447
4554
|
if (hasAgent) {
|
|
4448
4555
|
// 最后活动时间写入该 agent 自己的 runtime(不再落到 currentRuntime()/default)
|
|
4449
4556
|
try { engine.runtimeFor(agent).lastActiveAt = Date.now() } catch (e) {}
|
|
4557
|
+
// 全局活动戳同步更新(暂离检测统揽全局,见 tickTime)
|
|
4558
|
+
engine._globalLastActiveAt = Date.now()
|
|
4559
|
+
// 套娃防护(问题②):本插件 spawn 的子代理事件零处理——其 turn 结束不得再触发沉淀/路由
|
|
4560
|
+
try { if (engine._ownSubagents && engine._ownSubagents.has(agent)) return } catch (e) {}
|
|
4450
4561
|
try { engine.ingestAgentLifecycle(agent, 'agent/turn-stopping', { turn: payload && payload.turn, payload: { turn: payload && payload.turn } }) } catch (e) {}
|
|
4451
4562
|
}
|
|
4452
4563
|
diag('turn-stopping fired: turn=' + JSON.stringify(payload && payload.turn) + ' hasAgent=' + hasAgent + ' payloadKeys=' + (payload ? Object.keys(payload).join(',') : 'null'))
|
|
@@ -4464,8 +4575,12 @@ export function apply(ctx, config) {
|
|
|
4464
4575
|
ctx.on('agent/pre-step', async (payload, next) => {
|
|
4465
4576
|
try {
|
|
4466
4577
|
const agent = payload && payload.agent // M1: 精确取当前 agent 的 runtime,不猜
|
|
4578
|
+
// 套娃防护(问题②):本插件 spawn 的子代理 pre-step 零处理(不登记 runtime/不刷状态/不再触发任何派生)
|
|
4579
|
+
try { if (engine._ownSubagents && engine._ownSubagents.has(agent)) return next() } catch (e) {}
|
|
4467
4580
|
// 审查修复轮2:无 session 身份的 agent 不登记 runtime、不进 stateFor/refresh,直接放行
|
|
4468
4581
|
if (agent && engine.hasReliableSessionIdentity(agent)) {
|
|
4582
|
+
// 全局活动戳(pre-step 也是用户活动信号;暂离检测统揽全局)
|
|
4583
|
+
engine._globalLastActiveAt = Date.now()
|
|
4469
4584
|
// 首步即登记 runtime(重启恢复的会话也可能不触发 session-start,这里补登记)
|
|
4470
4585
|
const rt = engine.runtimeFor(agent)
|
|
4471
4586
|
// M7.5:pre-step 边界先冲刷 CoT 缓冲——保证本轮 context_push 的 window 携带最新思维链
|
|
@@ -5581,6 +5696,12 @@ export function apply(ctx, config) {
|
|
|
5581
5696
|
void (async () => {
|
|
5582
5697
|
try {
|
|
5583
5698
|
for (const rt of engine.runtimes.values()) {
|
|
5699
|
+
// 过期丢弃(问题③):队列条目存活 30 分钟,过期不再重试(防配置性错误无限循环 spawn)
|
|
5700
|
+
while (rt.pendingConsolidations.length) {
|
|
5701
|
+
const it = rt.pendingConsolidations[0]
|
|
5702
|
+
if (it && it.enqueuedAt && Date.now() - it.enqueuedAt > 30 * 60000) { rt.pendingConsolidations.shift(); try { if (rt._cqTries) rt._cqTries.delete(it.turn) } catch (e) {} continue }
|
|
5703
|
+
break
|
|
5704
|
+
}
|
|
5584
5705
|
if (rt.pendingConsolidations.length && !rt.consolidating) {
|
|
5585
5706
|
const item = rt.pendingConsolidations.shift()
|
|
5586
5707
|
if (item && item.agent) void engine.withAgent(item.agent, () => engine.consolidateTurn(item.turn, item.agent))
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a9i5k4/dsh-auto-memory",
|
|
3
3
|
"description": "主动联想记忆插件:记忆不靠模型调用,自己被唤回——Host 观察情境,NLP+向量化双轨检索,固定边界注入不破坏前缀缓存。三层记忆自动沉淀、欢迎向导、唤起回顾、无人值守、AI 问候与反思、日历、跨工具记忆继承。Proactive associative memory for DSH: zero-call recall, three-layer auto-consolidation, welcome tour, unattended mode.",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.36",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|