@a9i5k4/dsh-auto-memory 0.1.35 → 0.1.37
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/client.js +72 -5
- package/lib/index.js +198 -49
- package/lib/semantic-js.js +102 -3
- 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/client.js
CHANGED
|
@@ -495,6 +495,8 @@ window.__ModuleLoader__.load({
|
|
|
495
495
|
'补丁 0.1.32:「记忆唤起」开关与设置页双向联动(导览与设置走同一 semantic-emit 接口);语义引擎分级引导(无引擎自动装 JS 档,发烧友可选 Python)。',
|
|
496
496
|
'补丁 0.1.33:修复语义引擎下载 404(镜像 URL 重复前缀);transformers 推理库声明为可选依赖并随包分发。',
|
|
497
497
|
'补丁 0.1.34:修复推理库检测——pnpm 把 transformers 提升到 profile 根 node_modules,检测补该路径,新用户装完即用,无需手动操作。',
|
|
498
|
+
'补丁 0.1.37:修复「弹窗关不掉」(#13,感谢 @eclgo 报告)——暂离回归面板只在 away→active 翻转时弹一次;0.1.36 起回归检测全局化后,回归前(尚未发消息)面板会被每 30 秒强制重开,现已根治。',
|
|
499
|
+
'补丁 0.1.37:修复语义引擎「推理库缺失」误报 (#14,感谢 @moonltppt 报告)——推理库探测改用 Node 真实解析,覆盖 npm/pnpm(hoisted/isolated)全部安装布局;并新增「打开即自动检测」:检测不到自动深度扫描(~/.dsh/profiles 全家 + pnpm 虚拟存储),命中即热接入(无需重启),仍缺才推荐安装/一键下载,推荐卡 24 小时免打扰。',
|
|
498
500
|
], en: [
|
|
499
501
|
'★ MAJOR: Brand-new Welcome Tour — after first launch or upgrade, every feature is introduced step by step with per-feature switches right in the tour (association / snapshot / greeting / night unattended / reflection / summaries / external memory / skill promotion…); engine detection, download, self-test and live external-source scanning are all inline.',
|
|
500
502
|
'★ New brand visuals: an Office/Fluent-style liquid-glass app icon family — each step gets its own colored glass squircle icon (cyan inject / amber greeting / green calendar / violet engine / sky radar / coral finish) with dedicated looping motion (bell sway, page flip, linked rings, prism spin, radar sweep, rising spark); the signature three-slab frosted-glass logo stays on the Core step.',
|
|
@@ -508,6 +510,8 @@ window.__ModuleLoader__.load({
|
|
|
508
510
|
'Patch 0.1.32: the "Memory recall" switch now syncs bidirectionally with Settings (tour and settings share the semantic-emit endpoint); tiered engine onboarding (auto-install JS tier, Python optional for enthusiasts).',
|
|
509
511
|
'Patch 0.1.33: fixed semantic-engine download 404 (duplicate prefix in mirror URL); transformers inference lib declared as an optional dependency and shipped with the package.',
|
|
510
512
|
'Patch 0.1.34: fixed inference-lib detection — pnpm hoists transformers to the profile-root node_modules; detection now covers that path, so fresh installs work out of the box.',
|
|
513
|
+
'Patch 0.1.37: fixed the "popup cannot be closed" loop (#13, thanks @eclgo) — the welcome-back panel now opens only on the away→active transition; since 0.1.36 globalised away detection, the panel was force-reopened every 30s until the first message after returning. Root-caused and fixed.',
|
|
514
|
+
'Patch 0.1.37: fixed the false "inference runtime missing" state (#14, thanks @moonltppt) — runtime detection now uses real Node resolution, covering npm/pnpm (hoisted/isolated) layouts; plus a new check-on-open flow: when not detected, a deep scan (~/.dsh/profiles + pnpm virtual store) runs automatically and hot-wires hits with no restart; only if still missing does it recommend installing / one-click download, with a 24h snooze.',
|
|
511
515
|
] },
|
|
512
516
|
'0.1.29': { zh: [
|
|
513
517
|
'修复:工作区总览一直显示「未发现带记忆的工作区」——DSH 新版会话持久化改为 session.jsonl.zstd 压缩帧,旧逻辑只认 .jsonl 扫描不到任何会话;现已用 zstd 解压读取首行提取 cwd,并让空的 summary 缓存短时失效,避免空结果被永久固化。',
|
|
@@ -654,6 +658,7 @@ window.__ModuleLoader__.load({
|
|
|
654
658
|
if (d.kind === 'notice') return d.notice && d.notice.level === 'urgent' ? 80 : 70
|
|
655
659
|
if (d.kind === 'summary') return 60
|
|
656
660
|
if (d.kind === 'welcomeBack') return 50
|
|
661
|
+
if (d.kind === 'semSetup') return 45
|
|
657
662
|
return 10
|
|
658
663
|
}
|
|
659
664
|
function dialogKey(d) {
|
|
@@ -721,6 +726,8 @@ window.__ModuleLoader__.load({
|
|
|
721
726
|
greet: '/api/dsh-auto-memory/greet',
|
|
722
727
|
notices: '/api/dsh-auto-memory/notices',
|
|
723
728
|
models: '/api/dsh-auto-memory/models',
|
|
729
|
+
semanticDeepDetect: '/api/dsh-auto-memory/semantic-deep-detect',
|
|
730
|
+
semanticDownload: '/api/dsh-auto-memory/semantic-download',
|
|
724
731
|
}
|
|
725
732
|
function query(params) {
|
|
726
733
|
var search = new URLSearchParams()
|
|
@@ -2961,6 +2968,50 @@ window.__ModuleLoader__.load({
|
|
|
2961
2968
|
h('div', { style: { display: 'flex', justifyContent: 'flex-end', marginTop: '6px' } },
|
|
2962
2969
|
h('button', { 'data-dam-btn': '', style: close, onClick: closeDialog }, t('gotIt')))))
|
|
2963
2970
|
}
|
|
2971
|
+
if (dialogState.kind === 'semSetup') {
|
|
2972
|
+
// 打开即自动检测引导卡(0.1.37,#14 后续):快检+深扫仍未就绪时推荐补装/一键下载;
|
|
2973
|
+
// 深扫命中已热接入则报喜(即时生效)。关闭 = 24h 免打扰,不反复打扰。
|
|
2974
|
+
var sd = dialogState.result || {}
|
|
2975
|
+
var sdZh = locale === 'zh'
|
|
2976
|
+
var sdBody = { fontSize: 'calc(12px * var(--dam-scale))', lineHeight: 1.6, opacity: .92, whiteSpace: 'pre-wrap', marginTop: '4px' }
|
|
2977
|
+
var sdRows = []
|
|
2978
|
+
var sdActions = []
|
|
2979
|
+
var sdCmd = 'pnpm approve-builds\npnpm add @huggingface/transformers\n# 然后重启 dsh web'
|
|
2980
|
+
var sdSnooze = function () {
|
|
2981
|
+
try { localStorage.setItem('dsh-auto-memory.semDetectSnoozeUntil', String(Date.now() + 24 * 3600 * 1000)) } catch (eSn) {}
|
|
2982
|
+
closeDialog()
|
|
2983
|
+
}
|
|
2984
|
+
if (sd.deep && sd.deep.integrated) {
|
|
2985
|
+
sdRows.push(h('div', { style: sdBody }, sdZh
|
|
2986
|
+
? '检测到你已安装推理库 @huggingface/transformers(常规路径此前未识别,深度扫描已命中并自动接入)。语义检索 C2 现在即可用,无需重启。'
|
|
2987
|
+
: 'Your installed @huggingface/transformers was found by the deep scan and hot-wired automatically. The C2 semantic tier is ready now — no restart needed.'))
|
|
2988
|
+
} else {
|
|
2989
|
+
if (sd.recommendation === 'install-peer' || sd.recommendation === 'setup-both') {
|
|
2990
|
+
sdRows.push(h('div', { style: sdBody }, sdZh
|
|
2991
|
+
? (sd.recommendation === 'setup-both' ? '语义模型与推理库都未检测到。推理库请在安装 dsh-auto-memory 的同一目录(dsh profile)执行:' : '语义模型已下载,但推理库缺失(已含深度扫描)。请在安装 dsh-auto-memory 的同一目录(dsh profile)执行:')
|
|
2992
|
+
: (sd.recommendation === 'setup-both' ? 'Neither the model assets nor the inference runtime were found. For the runtime, run in the directory where dsh-auto-memory is installed (the dsh profile):' : 'Model assets found, but the inference runtime is missing (deep scan included). Run in the directory where dsh-auto-memory is installed (the dsh profile):')))
|
|
2993
|
+
sdActions.push(h('button', { 'data-dam-btn': '', style: close, onClick: function () {
|
|
2994
|
+
try { if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(sdCmd) } catch (eCp) {}
|
|
2995
|
+
} }, sdZh ? '复制命令' : 'Copy commands'))
|
|
2996
|
+
}
|
|
2997
|
+
if (sd.recommendation === 'download-model' || sd.recommendation === 'setup-both') {
|
|
2998
|
+
sdRows.push(h('div', { style: sdBody }, sdZh
|
|
2999
|
+
? '语义模型(内置 JS 档,约 135MB,一次即可)可一键后台下载,下载完成后语义检索自动启用:'
|
|
3000
|
+
: 'The built-in JS semantic model (~135MB, one-time) can be downloaded in the background; the semantic tier enables itself once done:'))
|
|
3001
|
+
sdActions.push(h('button', { 'data-dam-btn': '', 'data-primary': 'true', style: close, onClick: function () {
|
|
3002
|
+
apiPost(API.semanticDownload, { action: 'start', mirror: 'auto' }).catch(function () {})
|
|
3003
|
+
closeDialog()
|
|
3004
|
+
} }, sdZh ? '立即下载' : 'Download now'))
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
return h('div', { style: overlay },
|
|
3008
|
+
h('div', { style: box },
|
|
3009
|
+
h('div', { style: head }, sdZh ? '自动记忆引擎检测' : 'Memory engine check'),
|
|
3010
|
+
sdRows,
|
|
3011
|
+
h('div', { style: { display: 'flex', gap: '8px', justifyContent: 'flex-end', marginTop: '6px' } },
|
|
3012
|
+
sdActions,
|
|
3013
|
+
h('button', { 'data-dam-btn': '', style: close, onClick: (sd.deep && sd.deep.integrated) ? closeDialog : sdSnooze }, t('gotIt')))))
|
|
3014
|
+
}
|
|
2964
3015
|
var versions = dialogState.versions || []
|
|
2965
3016
|
var lastV = versions.length ? versions[versions.length - 1].version : ''
|
|
2966
3017
|
return h('div', { style: overlay },
|
|
@@ -3505,8 +3556,10 @@ window.__ModuleLoader__.load({
|
|
|
3505
3556
|
})
|
|
3506
3557
|
// 语言切换时通知所有订阅者重渲染
|
|
3507
3558
|
onLocale(function () { emit() })
|
|
3508
|
-
// 暂离回来自动弹开记忆窗口:host 判定暂离(阈值可配 awayMinutes)
|
|
3509
|
-
//
|
|
3559
|
+
// 暂离回来自动弹开记忆窗口:host 判定暂离(阈值可配 awayMinutes),轮询发现回归(true→false)时自动打开。
|
|
3560
|
+
// v0.1.37 修复(#13):本函数不得做「away 且面板关着就再弹开」——≤0.1.35 host 侧 away 无写入点恒 false
|
|
3561
|
+
// 掩盖了旧行,0.1.36 全局 away 激活后它变成 30s 重开死循环:用户回到机器但还没发消息时 host 恒判 away,
|
|
3562
|
+
// 关掉即被重弹(=「弹窗关不掉」)。契约以设置页文案为准:回归(away true→false)才弹,away 挂着绝不强弹。
|
|
3510
3563
|
var prevAwayState = null
|
|
3511
3564
|
function autoOpenOnReturn() {
|
|
3512
3565
|
try {
|
|
@@ -3517,11 +3570,8 @@ window.__ModuleLoader__.load({
|
|
|
3517
3570
|
var pend = lastPendingSummary
|
|
3518
3571
|
if (pend) { openDialog({ kind: 'summary', summary: pend }); lastPendingSummary = null }
|
|
3519
3572
|
else if (!document.hidden) openDialog({ kind: 'welcomeBack' })
|
|
3520
|
-
prevAwayState = hostAway
|
|
3521
|
-
return
|
|
3522
3573
|
}
|
|
3523
3574
|
prevAwayState = hostAwayReady ? hostAway : prevAwayState
|
|
3524
|
-
if (isAway() && !controller.isOpen()) controller.open()
|
|
3525
3575
|
} catch (e) {}
|
|
3526
3576
|
}
|
|
3527
3577
|
autoOpenOnReturn()
|
|
@@ -3595,6 +3645,23 @@ window.__ModuleLoader__.load({
|
|
|
3595
3645
|
checkNotices()
|
|
3596
3646
|
var noticesTimer = setInterval(checkNotices, 3600 * 1000)
|
|
3597
3647
|
|
|
3648
|
+
// 打开即自动检测语义引擎(0.1.37,#14 后续):每次打开页面一次「快检 → 失败自动深扫
|
|
3649
|
+
// (~/.dsh/profiles 全家 + pnpm 虚拟存储) → 仍缺才推荐」。深扫命中即热接入(即时生效);
|
|
3650
|
+
// 推荐卡关闭 = 24h 免打扰。延迟 12s 错开开屏的向导/changelog 弹窗(优先级本身也压它一头)。
|
|
3651
|
+
setTimeout(function () {
|
|
3652
|
+
apiGet(API.semanticDeepDetect).then(function (d) {
|
|
3653
|
+
try {
|
|
3654
|
+
if (!d || d.ready) return
|
|
3655
|
+
if (!(d.deep && d.deep.integrated)) {
|
|
3656
|
+
var sdUntil = 0
|
|
3657
|
+
try { sdUntil = Number(localStorage.getItem('dsh-auto-memory.semDetectSnoozeUntil') || 0) } catch (eSn) {}
|
|
3658
|
+
if (Date.now() < sdUntil) return
|
|
3659
|
+
}
|
|
3660
|
+
openDialog({ kind: 'semSetup', result: d })
|
|
3661
|
+
} catch (eSD) {}
|
|
3662
|
+
}).catch(function () {})
|
|
3663
|
+
}, 12000)
|
|
3664
|
+
|
|
3598
3665
|
// 时间检测轮询(30s):拉 host 的暂离状态/待展示总结,检测暂离回归并自动弹窗
|
|
3599
3666
|
var lastPendingSummary = null
|
|
3600
3667
|
var awayPollTimer = null
|
package/lib/index.js
CHANGED
|
@@ -32,7 +32,7 @@ import { createContextHost } from './context-host.js'
|
|
|
32
32
|
import { createSuccessEvidencePre } from './context-bridge.js'
|
|
33
33
|
import { createIndexSyncHostPre } from './m7-index-sync-host.js'
|
|
34
34
|
import { createActivationHost } from './activation-host.js'
|
|
35
|
-
import { createJsSemanticEnginePre, createSemanticDownloaderPre, fuseD6Pre, E5_SMALL_Q8_MANIFEST_V1 } from './semantic-js.js'
|
|
35
|
+
import { createJsSemanticEnginePre, createSemanticDownloaderPre, fuseD6Pre, probeJsSemanticAssets, deepScanPeerTransformers, E5_SMALL_Q8_MANIFEST_V1 } from './semantic-js.js'
|
|
36
36
|
import { loadAndVerifyPolicy, decideActivationV2, lexicalContainment } from './semantic-decide.js'
|
|
37
37
|
import { createPythonSidecarClientPre, defaultWorkerScriptPathPre } from './python-sidecar-client.js'
|
|
38
38
|
import { createEpisodicStorePre } from './episodic-store.js'
|
|
@@ -93,6 +93,7 @@ export const API = {
|
|
|
93
93
|
notices: '/api/dsh-auto-memory/notices',
|
|
94
94
|
'activation-inbox': '/api/dsh-auto-memory/activation-inbox',
|
|
95
95
|
'semantic-status': '/api/dsh-auto-memory/semantic-status',
|
|
96
|
+
'semantic-deep-detect': '/api/dsh-auto-memory/semantic-deep-detect',
|
|
96
97
|
'semantic-download': '/api/dsh-auto-memory/semantic-download',
|
|
97
98
|
'semantic-emit': '/api/dsh-auto-memory/semantic-emit',
|
|
98
99
|
'shadow-recent': '/api/dsh-auto-memory/shadow-recent',
|
|
@@ -668,6 +669,10 @@ class MemoryEngine {
|
|
|
668
669
|
this._autoCallDate = ''
|
|
669
670
|
this._autoCallCount = 0
|
|
670
671
|
this._lastConsolidateStartedAt = 0
|
|
672
|
+
this._globalLastActiveAt = 0 // 全局活动时间戳:任意会话/工作区任一活动即更新(暂离检测统揽全局,不按工作区)
|
|
673
|
+
this._ownSubagents = new WeakSet() // 本插件 spawn 的子代理(防套娃:其生命周期事件零处理;WeakSet 不阻止 GC)
|
|
674
|
+
this._subagentInflight = 0 // 子代理并发闸门(启动瞬间多路 spawn 叠加是卡顿根因之一)
|
|
675
|
+
this._subagentCircuit = null // 配置性错误熔断 { until, reason }(UNKNOWN_MODEL 等,重试无意义)
|
|
671
676
|
this._smartRecallFlight = undefined
|
|
672
677
|
this._budgets = undefined // 每日写入预算:用户级4000/项目级3000字/天(所有会话共享,跨天重置)
|
|
673
678
|
// M2: 进程级观察统计(无 owner 而无法归属 runtime 的事件只在这里留痕,绝不落入 default runtime)
|
|
@@ -1375,6 +1380,42 @@ class MemoryEngine {
|
|
|
1375
1380
|
}
|
|
1376
1381
|
}
|
|
1377
1382
|
|
|
1383
|
+
/** 全局层路径:每日总结/时段问候跨工作区统揽(不落在任何单一工作区)。 */
|
|
1384
|
+
globalPaths() {
|
|
1385
|
+
const userDir = this.userDirOf()
|
|
1386
|
+
return {
|
|
1387
|
+
userDir,
|
|
1388
|
+
summariesDir: path.join(userDir, 'summaries'),
|
|
1389
|
+
greetDir: path.join(userDir, 'greetings'),
|
|
1390
|
+
greetPath: path.join(userDir, 'greetings', `${this.memToday()}.json`),
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
/**
|
|
1395
|
+
* 全局日志扫描:聚合所有工作区指定日期的日志条目(统揽全局,而非单工作区)。
|
|
1396
|
+
* 返回 [{ ws, date, text, lines:[...] }],按工作区名排序,读文件失败的工作区跳过。
|
|
1397
|
+
*/
|
|
1398
|
+
async listAllWorkspaceLogs(date) {
|
|
1399
|
+
const out = []
|
|
1400
|
+
try {
|
|
1401
|
+
if (!this.configLoaded) { try { await this.loadConfig() } catch (e) {} }
|
|
1402
|
+
const root = this.expandUserPath(this.config.memoryRoot) || path.join(dshHome(), 'memory', 'workspaces')
|
|
1403
|
+
let entries = []
|
|
1404
|
+
try { entries = await readdir(root, { withFileTypes: true }) } catch (e) { return out }
|
|
1405
|
+
for (const ent of entries) {
|
|
1406
|
+
if (!ent.isDirectory()) continue
|
|
1407
|
+
const ws = ent.name
|
|
1408
|
+
const logFile = path.join(root, ws, `${date}.md`)
|
|
1409
|
+
let text = ''
|
|
1410
|
+
try { text = await this.readTextSafe(logFile) } catch (e) { continue }
|
|
1411
|
+
if (!text) continue
|
|
1412
|
+
out.push({ ws, date, text, lines: text.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('- ')).map((l) => l.replace(/^- /, '')) })
|
|
1413
|
+
}
|
|
1414
|
+
} catch (e) {}
|
|
1415
|
+
out.sort((a, b) => String(a.ws).localeCompare(String(b.ws)))
|
|
1416
|
+
return out
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1378
1419
|
// ---------- 更新检查(registry 自动检查 + profile 探测) ----------
|
|
1379
1420
|
/** 找 dsh web profile 目录(含 dsh-auto-memory 依赖/bundle 的那个)。 */
|
|
1380
1421
|
async findProfileDir() {
|
|
@@ -1655,6 +1696,15 @@ class MemoryEngine {
|
|
|
1655
1696
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
1656
1697
|
}
|
|
1657
1698
|
|
|
1699
|
+
/** 记忆日偏移:memToday() 基础上加减 N 天(供"昨天"日志回看)。 */
|
|
1700
|
+
memTodayOfOffset(days) {
|
|
1701
|
+
const s = this.memToday()
|
|
1702
|
+
const [y, m, dd] = s.split('-').map(Number)
|
|
1703
|
+
const d = new Date(y, m - 1, dd)
|
|
1704
|
+
d.setDate(d.getDate() + Number(days || 0))
|
|
1705
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1658
1708
|
/**
|
|
1659
1709
|
* 是否应处于无人值守模式(2026-08-26,roadmap「自动检测」)。
|
|
1660
1710
|
* 优先级:手动 unattendedMode=true → 恒 true;否则 unattendedAuto=true 且(命中非工作时间窗
|
|
@@ -1949,7 +1999,9 @@ class MemoryEngine {
|
|
|
1949
1999
|
}
|
|
1950
2000
|
// 暂离回来提示:距上次活动>1小时,要求 agent 在回复开头写欢迎语并提示打开记忆窗口
|
|
1951
2001
|
// ——无人值守模式下剥离(不注入社交性/行为性指令,避免无人值守任务浪费 token 在寒暄上)
|
|
1952
|
-
|
|
2002
|
+
let lastActiveGlobal = this._globalLastActiveAt || 0
|
|
2003
|
+
try { for (const rt of this.runtimes.values()) { const t = Number(rt.lastActiveAt) || 0; if (t > lastActiveGlobal) lastActiveGlobal = t } } catch (e) {}
|
|
2004
|
+
if (!this.isUnattendedNow() && lastActiveGlobal && Date.now() - lastActiveGlobal > 3600000) {
|
|
1953
2005
|
lines.push('\n' + L('snapshotWelcomeTitle'))
|
|
1954
2006
|
lines.push(L('snapshotWelcomeBody'))
|
|
1955
2007
|
}
|
|
@@ -2679,16 +2731,11 @@ class MemoryEngine {
|
|
|
2679
2731
|
|
|
2680
2732
|
/** 生活化总结:调用 DSH 的 AI(subagent)发散地总结某时段的工作(完成/收尾/烦恼/暂停的事)。 */
|
|
2681
2733
|
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) {}
|
|
2734
|
+
// 全局统揽(2026-09-01):时段总结不再绑定单一工作区——聚合所有工作区的日志,
|
|
2735
|
+
// 源文件列表与缓存都走用户级目录(~/.dsh/memory/summaries/),换工作区总结内容与缓存不变。
|
|
2736
|
+
const today = this.memToday()
|
|
2737
|
+
const gp = this.globalPaths()
|
|
2738
|
+
const cacheFile = path.join(gp.summariesDir, today + '-' + period + '.json')
|
|
2692
2739
|
if (!force && cacheFile) {
|
|
2693
2740
|
try {
|
|
2694
2741
|
const raw = await this.readTextSafe(cacheFile)
|
|
@@ -2698,14 +2745,27 @@ class MemoryEngine {
|
|
|
2698
2745
|
}
|
|
2699
2746
|
} catch (e) {}
|
|
2700
2747
|
}
|
|
2701
|
-
|
|
2702
|
-
const
|
|
2703
|
-
|
|
2704
|
-
|
|
2748
|
+
// 候选条目:全局扫描所有工作区(当前时段用今天的日志,昨天用昨天的)
|
|
2749
|
+
const scanDate = (period === '昨天') ? (this.state.recentLogs[0] ? this.state.recentLogs[0].date : this.memTodayOfOffset(-1)) : today
|
|
2750
|
+
const wsLogs = await this.listAllWorkspaceLogs(scanDate)
|
|
2751
|
+
// 兼容:若全局扫描为空(如记忆根迁移中),降级到当前工作区旧路径
|
|
2752
|
+
let items = wsLogs.flatMap((w) => w.lines)
|
|
2753
|
+
let sourceFiles = wsLogs.map((w) => path.join(this.expandUserPath(this.config.memoryRoot) || path.join(dshHome(), 'memory', 'workspaces'), w.ws, `${w.date}.md`))
|
|
2754
|
+
if (!wsLogs.length) {
|
|
2755
|
+
try {
|
|
2756
|
+
const paths = await this.resolvePaths(agent)
|
|
2757
|
+
const legacy = (period === '昨天')
|
|
2758
|
+
? (this.state.recentLogs[0] ? [{ file: path.join(paths.projectDir, this.state.recentLogs[0].date + '.md'), text: this.state.recentLogs[0].text }] : [])
|
|
2759
|
+
: [{ file: paths.logPath, text: '' }]
|
|
2760
|
+
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] || []
|
|
2761
|
+
sourceFiles = legacy.map((x) => x.file)
|
|
2762
|
+
} catch (e) {}
|
|
2763
|
+
}
|
|
2764
|
+
if (!items.length) return { summary: '', works: [], cached: true, generatedAt: Date.now() }
|
|
2705
2765
|
const prompt = [
|
|
2706
|
-
'
|
|
2707
|
-
|
|
2708
|
-
'
|
|
2766
|
+
'你是一个温暖、细腻的生活助理。请使用文件读取工具依次读取下面的绝对路径(跨多个工作区的记忆日志,请统揽全局),不要要求用户粘贴内容:',
|
|
2767
|
+
...sourceFiles.map((f, i) => (i + 1) + '. ' + f),
|
|
2768
|
+
'从这些文件中只筛选“' + period + '”时段的工作条目(昨天则读取整份昨天日志),共有约 ' + items.length + ' 条候选记录。',
|
|
2709
2769
|
'1. 写一段生活化总结(80-160字),像朋友聊天:概括完成的事、是否收尾、可能的烦恼(温和带过,看不出就跳过);不要列表、不要小标题、不要"总结:"前缀。',
|
|
2710
2770
|
'2. 把原始记录归纳成若干项“工作”(3-6项),每项给简短标题(8-16字),并列出细点(每项2-4条)。',
|
|
2711
2771
|
'输出格式(严格遵守,不要多余文字):',
|
|
@@ -2740,9 +2800,15 @@ class MemoryEngine {
|
|
|
2740
2800
|
// ---------- 时间检测(每 15s 心跳 tick):暂离状态 / _lastAgent 定时兜底 / 自动总结时间点 ----------
|
|
2741
2801
|
tickTime() {
|
|
2742
2802
|
try {
|
|
2743
|
-
// 1)
|
|
2803
|
+
// 1) 暂离检测(全局统揽):取所有 runtime 里最近一次活动时间(跨工作区),而非当前 runtime 的孤立时间戳。
|
|
2804
|
+
let lastActive = this._globalLastActiveAt || 0
|
|
2805
|
+
try { for (const rt of this.runtimes.values()) { const t = Number(rt.lastActiveAt) || 0; if (t > lastActive) lastActive = t } } catch (e) {}
|
|
2806
|
+
this._globalLastActiveAt = lastActive
|
|
2744
2807
|
const awayMs = Math.max(Number(this.config.awayMinutes) || 60, 1) * 60000
|
|
2745
|
-
|
|
2808
|
+
const away = !!(lastActive && Date.now() - lastActive > awayMs)
|
|
2809
|
+
// 广播到所有 runtime(面板/注入读各自 runtime 的 state.away,值一致=全局语义)
|
|
2810
|
+
try { for (const rt of this.runtimes.values()) rt.state.away = away } catch (e) {}
|
|
2811
|
+
this.state.away = away
|
|
2746
2812
|
// 2) _lastAgent 定时兜底(重启恢复会话不触发 session-start;pre-step 兜底之外的第二保险)
|
|
2747
2813
|
if (!this._lastAgent) this.restoreLastAgent()
|
|
2748
2814
|
// 3) 自动总结时间点:命中配置的 HH:MM 且当天未生成过 → 生成时段总结并标记展示
|
|
@@ -2811,8 +2877,9 @@ class MemoryEngine {
|
|
|
2811
2877
|
const hour = new Date().getHours()
|
|
2812
2878
|
const seg = hour < 6 ? 'morning' : hour < 9 ? 'morning' : hour < 12 ? 'forenoon' : hour < 14 ? 'noon' : hour < 18 ? 'afternoon' : 'evening'
|
|
2813
2879
|
const segLabel = { morning: '早上', forenoon: '上午', noon: '中午', afternoon: '下午', evening: '晚上' }[seg]
|
|
2814
|
-
|
|
2815
|
-
const
|
|
2880
|
+
// 全局统揽(2026-09-01):问候缓存迁用户级目录(跨工作区共享同一份),引用条目聚合全部工作区今日日志
|
|
2881
|
+
const gp = this.globalPaths()
|
|
2882
|
+
const greetFile = path.join(gp.greetDir, this.memToday() + '.json')
|
|
2816
2883
|
// 已有该时段缓存 → 直接返回
|
|
2817
2884
|
try {
|
|
2818
2885
|
const raw = await this.readTextSafe(greetFile)
|
|
@@ -2821,8 +2888,13 @@ class MemoryEngine {
|
|
|
2821
2888
|
if (j && j[seg]) return { greeting: j[seg], cached: true }
|
|
2822
2889
|
}
|
|
2823
2890
|
} catch (e) {}
|
|
2824
|
-
// 今日记录(
|
|
2825
|
-
const
|
|
2891
|
+
// 今日记录(供问候引用最近几条;聚合所有工作区,统揽全局)
|
|
2892
|
+
const wsLogs = await this.listAllWorkspaceLogs(this.memToday())
|
|
2893
|
+
let items = wsLogs.flatMap((w) => w.lines.map((l) => l.replace(/^- \d{2}:\d{2} /, '').replace(/^- /, ''))).slice(-6)
|
|
2894
|
+
if (!items.length) {
|
|
2895
|
+
// 兼容降级:全局扫描为空时回落当前工作区
|
|
2896
|
+
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)
|
|
2897
|
+
}
|
|
2826
2898
|
const prompt = [
|
|
2827
2899
|
'你是一个温暖的生活助理。现在是' + segLabel + ',请给用户写一句简短的拟人化问候(30-60字),像朋友一样:',
|
|
2828
2900
|
'- 按时间段自然问候(' + segLabel + '好/辛苦了之类),自然提起今天完成的主要工作(挑1-2件最重要的),可以带一句贴心提醒(如早点休息)',
|
|
@@ -2838,7 +2910,7 @@ class MemoryEngine {
|
|
|
2838
2910
|
let all = {}
|
|
2839
2911
|
try { const raw = await this.readTextSafe(greetFile); if (raw) { const j = JSON.parse(raw); if (j) all = j } } catch (e) {}
|
|
2840
2912
|
all[seg] = text
|
|
2841
|
-
try { await mkdir(
|
|
2913
|
+
try { await mkdir(gp.greetDir, { recursive: true }); await writeFile(greetFile, JSON.stringify(all, null, 2), 'utf8') } catch (e) {}
|
|
2842
2914
|
return { greeting: text, cached: false }
|
|
2843
2915
|
}
|
|
2844
2916
|
|
|
@@ -2846,6 +2918,18 @@ class MemoryEngine {
|
|
|
2846
2918
|
async runSubagent(text, label, agent, timeoutMs) {
|
|
2847
2919
|
const subagents = this._subagents
|
|
2848
2920
|
if (!subagents) return ''
|
|
2921
|
+
// 配置性错误熔断(问题③):UNKNOWN_MODEL 等错误不会自愈,熔断期内直接返回空,不再反复 spawn(卡顿根因)
|
|
2922
|
+
try {
|
|
2923
|
+
if (this._subagentCircuit && Date.now() < this._subagentCircuit.until) {
|
|
2924
|
+
diag('subagent ' + label + ' circuit-open: ' + this._subagentCircuit.reason)
|
|
2925
|
+
return ''
|
|
2926
|
+
}
|
|
2927
|
+
} catch (e) {}
|
|
2928
|
+
// 并发闸门(问题②):同时在飞的子代理 ≥3 时拒绝新 spawn(启动瞬间多路叠加导致卡顿)
|
|
2929
|
+
if (this._subagentInflight >= 3) {
|
|
2930
|
+
diag('subagent ' + label + ' skipped: inflight=' + this._subagentInflight)
|
|
2931
|
+
return ''
|
|
2932
|
+
}
|
|
2849
2933
|
// parent 必须是完整 agent 对象(captureDelegatedPolicyOverrides 读 parent.ctx/parent.session);路由调用用缓存的最近 agent
|
|
2850
2934
|
const parent = agent || this._lastAgent
|
|
2851
2935
|
if (!parent || !parent.session || !parent.ctx || typeof parent.ctx.get !== 'function') {
|
|
@@ -2865,6 +2949,7 @@ class MemoryEngine {
|
|
|
2865
2949
|
try {
|
|
2866
2950
|
// prompt 必须是 block 数组(createUserMessage 校验 content.some)
|
|
2867
2951
|
const subModel = String(this.config.subagentModel || '').trim()
|
|
2952
|
+
this._subagentInflight = (this._subagentInflight || 0) + 1
|
|
2868
2953
|
run = await subagents.start(providerName, {
|
|
2869
2954
|
label,
|
|
2870
2955
|
prompt: [{ type: 'text', text }],
|
|
@@ -2873,6 +2958,8 @@ class MemoryEngine {
|
|
|
2873
2958
|
// 设置页「总结/问候默认模型」:非空时覆盖子代理的模型选择(空=跟随路由默认)
|
|
2874
2959
|
...(subModel ? { agentOptions: { model: subModel } } : {}),
|
|
2875
2960
|
})
|
|
2961
|
+
// 套娃防护(问题②):登记本插件 spawn 的子代理,其生命周期事件在 pre-step/turn-stopping 零处理
|
|
2962
|
+
try { if (run && run.agent) this._ownSubagents.add(run.agent) } catch (eReg) {}
|
|
2876
2963
|
// UNKNOWN_MODEL 自保:子代理路由为两源拼接(provider 继承父会话 + 模型落宿主全局默认),
|
|
2877
2964
|
// 父 provider 目录可能不含该模型(如 opencode-go-free × deepseek-v4-flash);
|
|
2878
2965
|
// 此时回退用父会话抽屉当前模型(settings.yaml agent-default-model)重试一次。
|
|
@@ -2910,8 +2997,15 @@ class MemoryEngine {
|
|
|
2910
2997
|
const em = e && e.message ? e.message : String(e)
|
|
2911
2998
|
console.error('[dsh-auto-memory] ' + label + ' failed', em)
|
|
2912
2999
|
diag('subagent ' + label + ' failed: ' + em)
|
|
3000
|
+
// 配置性错误熔断(问题③):UNKNOWN_MODEL / 无配置模型 / 供应商不含该模型——重试无意义,30 分钟内不再 spawn
|
|
3001
|
+
// (匹配收紧:避免普通网络错误的 message 恰含 "provider" 字样被误熔断)
|
|
3002
|
+
if (/UNKNOWN_MODEL|has no configured model|no configured provider|provider.*(not|no).*(config|model)|cannot find package/i.test(em)) {
|
|
3003
|
+
this._subagentCircuit = { until: Date.now() + 30 * 60000, reason: em.slice(0, 120) }
|
|
3004
|
+
diag('subagent circuit OPEN for 30min: ' + em.slice(0, 120))
|
|
3005
|
+
}
|
|
2913
3006
|
return ''
|
|
2914
3007
|
} finally {
|
|
3008
|
+
this._subagentInflight = Math.max(0, (this._subagentInflight || 1) - 1)
|
|
2915
3009
|
clearTimeout(timer)
|
|
2916
3010
|
if (controller.signal.aborted && run && typeof run.dispose === 'function') {
|
|
2917
3011
|
try { await run.dispose() } catch (e2) {}
|
|
@@ -3077,11 +3171,21 @@ class MemoryEngine {
|
|
|
3077
3171
|
// 超时兜底:subagent 挂起(如会话收尾期)时 40s 后放弃并走重试队列,避免 _consolidating 永久占用导致后续轮次全部 skip busy
|
|
3078
3172
|
const text = await this.withTimeout(this.runSubagent(prompt, 'auto-memory-consolidate', agent), 40000, '')
|
|
3079
3173
|
if (!text) {
|
|
3174
|
+
// 配置性错误熔断中(问题③):UNKNOWN_MODEL 等不会自愈,不入队重试(重试=反复 spawn 卡顿)
|
|
3175
|
+
if (this._subagentCircuit && Date.now() < this._subagentCircuit.until) {
|
|
3176
|
+
diag('consolidate: circuit open (' + this._subagentCircuit.reason + ') — not queued')
|
|
3177
|
+
return
|
|
3178
|
+
}
|
|
3080
3179
|
diag('consolidate: subagent returned empty text (queued for retry)')
|
|
3081
|
-
// subagent 失败(返回空)
|
|
3082
|
-
|
|
3180
|
+
// subagent 失败(返回空):入重试队列,由后台轮询兜底重试;每条最多 3 次、存活 30 分钟(过期丢弃)
|
|
3181
|
+
const item = { turn, agent, tries: ((runtime._cqTries && runtime._cqTries.get(turn)) || 0) + 1, enqueuedAt: Date.now() }
|
|
3182
|
+
runtime._cqTries = runtime._cqTries || new Map()
|
|
3183
|
+
runtime._cqTries.set(turn, item.tries)
|
|
3184
|
+
if (item.tries <= 3 && runtime.pendingConsolidations.length < 5) runtime.pendingConsolidations.push(item)
|
|
3185
|
+
else { try { runtime._cqTries.delete(turn) } catch (e) {} }
|
|
3083
3186
|
return
|
|
3084
3187
|
}
|
|
3188
|
+
try { if (runtime._cqTries) runtime._cqTries.delete(turn) } catch (e) {}
|
|
3085
3189
|
if (text.includes('(无)')) return
|
|
3086
3190
|
const logPts = []
|
|
3087
3191
|
const notePts = []
|
|
@@ -3373,6 +3477,10 @@ class MemoryEngine {
|
|
|
3373
3477
|
// 时间检测:暂离状态 / 待展示的自动总结 / 相关配置
|
|
3374
3478
|
away: !!this.state.away,
|
|
3375
3479
|
awayMinutes: Math.max(Number(this.config.awayMinutes) || 60, 1),
|
|
3480
|
+
// 子代理熔断状态(问题③):配置性错误(如所选模型不在供应商目录)时置位,30 分钟后自动恢复
|
|
3481
|
+
subagentCircuit: (this._subagentCircuit && Date.now() < this._subagentCircuit.until)
|
|
3482
|
+
? { until: this._subagentCircuit.until, reason: this._subagentCircuit.reason }
|
|
3483
|
+
: null,
|
|
3376
3484
|
autoPopupEnabled: this.config.autoPopupEnabled !== false,
|
|
3377
3485
|
autoSummaryTimes: Array.isArray(this.config.autoSummaryTimes) ? this.config.autoSummaryTimes : [],
|
|
3378
3486
|
pendingSummary: this.state.pendingSummary || null,
|
|
@@ -4245,28 +4353,45 @@ export function apply(ctx, config) {
|
|
|
4245
4353
|
const pluginDir = path.dirname(fileURLToPath(import.meta.url))
|
|
4246
4354
|
engine._jsSemantic = createJsSemanticEnginePre({ pluginDir })
|
|
4247
4355
|
engine._jsDownload = createSemanticDownloaderPre({ modelsRoot: path.join(pluginDir, 'models') })
|
|
4248
|
-
// 资产探测(semantic-status / 档位解析 / 引导卡共用)
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
4263
|
-
|
|
4356
|
+
// 资产探测(semantic-status / 档位解析 / 引导卡共用):共享实现见 semantic-js-pre 的
|
|
4357
|
+
// probeJsSemanticAssets —— peer 探测与引擎加载走同一套 Node 解析(2026-09-02 issue 修正)。
|
|
4358
|
+
// _peerExtraDirs = 深度扫描热接入位(semanticDeepDetect 命中后 probe/加载/档位即时生效)。
|
|
4359
|
+
engine._peerExtraDirs = []
|
|
4360
|
+
engine.semanticAssetProbe = async () => probeJsSemanticAssets(pluginDir, engine._peerExtraDirs)
|
|
4361
|
+
// 打开即自动检测(0.1.37,#14 后续):快检 → 模型在场但推理库缺失时深度扫描
|
|
4362
|
+
// (~/.dsh/profiles/* 全家 + pnpm 虚拟存储) → 命中即热接入(probe/加载双注入,无需重启),
|
|
4363
|
+
// 并给出 recommendation 供引导卡分流(none/setup-both/download-model/install-peer)。
|
|
4364
|
+
engine.semanticDeepDetect = async () => {
|
|
4365
|
+
const quick = await engine.semanticAssetProbe()
|
|
4366
|
+
const deep = { scanned: false, foundDirs: [], integrated: false }
|
|
4367
|
+
if (!quick.ready && quick.assetPresent) {
|
|
4368
|
+
deep.scanned = true
|
|
4369
|
+
try {
|
|
4370
|
+
const osMod = await import('node:os')
|
|
4371
|
+
const profilesRoot = path.join(osMod.homedir(), '.dsh', 'profiles')
|
|
4372
|
+
const found = deepScanPeerTransformers([path.join(pluginDir, '..'), profilesRoot])
|
|
4373
|
+
deep.foundDirs = found
|
|
4374
|
+
for (const d of found) {
|
|
4375
|
+
deep.integrated = true
|
|
4376
|
+
if (!engine._peerExtraDirs.includes(d)) engine._peerExtraDirs.push(d)
|
|
4377
|
+
try { if (engine._jsSemantic && engine._jsSemantic.addPeerDirCandidates) engine._jsSemantic.addPeerDirCandidates([d]) } catch (_) {}
|
|
4378
|
+
}
|
|
4379
|
+
} catch (_) { /* 扫描失败按未命中处理 */ }
|
|
4380
|
+
}
|
|
4381
|
+
const after = await engine.semanticAssetProbe()
|
|
4382
|
+
const recommendation = after.ready ? 'none'
|
|
4383
|
+
: (!after.assetPresent && !after.peerPresent) ? 'setup-both'
|
|
4384
|
+
: (!after.peerPresent) ? 'install-peer'
|
|
4385
|
+
: 'download-model'
|
|
4264
4386
|
return {
|
|
4265
|
-
assetPresent,
|
|
4266
|
-
peerPresent:
|
|
4267
|
-
ready:
|
|
4268
|
-
assetBytes:
|
|
4269
|
-
assetPath:
|
|
4387
|
+
assetPresent: after.assetPresent,
|
|
4388
|
+
peerPresent: after.peerPresent,
|
|
4389
|
+
ready: after.ready,
|
|
4390
|
+
assetBytes: after.assetBytes,
|
|
4391
|
+
assetPath: after.assetPath,
|
|
4392
|
+
resolvedTier: await engine.resolveSemanticTier(),
|
|
4393
|
+
deep,
|
|
4394
|
+
recommendation,
|
|
4270
4395
|
}
|
|
4271
4396
|
}
|
|
4272
4397
|
// 档位解析:lexical→C1 强制保底;其余(auto/js/python)在资产就绪时启用 C2 臂
|
|
@@ -4445,8 +4570,13 @@ export function apply(ctx, config) {
|
|
|
4445
4570
|
// 审查修复轮3:有效性判断必须是严格 session 身份 —— !!agent.session 会放行无身份对象
|
|
4446
4571
|
const hasAgent = !!(agent && engine.hasReliableSessionIdentity(agent))
|
|
4447
4572
|
if (hasAgent) {
|
|
4573
|
+
// 套娃防护(问题②):本插件 spawn 的子代理事件零处理——其 turn 结束不得再触发沉淀/路由,
|
|
4574
|
+
// 也不得刷新活动戳(v0.1.37 修正:后台沉淀/定时总结若刷新活动戳,会把真暂离误翻成「回归」)
|
|
4575
|
+
try { if (engine._ownSubagents && engine._ownSubagents.has(agent)) return } catch (e) {}
|
|
4448
4576
|
// 最后活动时间写入该 agent 自己的 runtime(不再落到 currentRuntime()/default)
|
|
4449
4577
|
try { engine.runtimeFor(agent).lastActiveAt = Date.now() } catch (e) {}
|
|
4578
|
+
// 全局活动戳同步更新(暂离检测统揽全局,见 tickTime)
|
|
4579
|
+
engine._globalLastActiveAt = Date.now()
|
|
4450
4580
|
try { engine.ingestAgentLifecycle(agent, 'agent/turn-stopping', { turn: payload && payload.turn, payload: { turn: payload && payload.turn } }) } catch (e) {}
|
|
4451
4581
|
}
|
|
4452
4582
|
diag('turn-stopping fired: turn=' + JSON.stringify(payload && payload.turn) + ' hasAgent=' + hasAgent + ' payloadKeys=' + (payload ? Object.keys(payload).join(',') : 'null'))
|
|
@@ -4464,8 +4594,12 @@ export function apply(ctx, config) {
|
|
|
4464
4594
|
ctx.on('agent/pre-step', async (payload, next) => {
|
|
4465
4595
|
try {
|
|
4466
4596
|
const agent = payload && payload.agent // M1: 精确取当前 agent 的 runtime,不猜
|
|
4597
|
+
// 套娃防护(问题②):本插件 spawn 的子代理 pre-step 零处理(不登记 runtime/不刷状态/不再触发任何派生)
|
|
4598
|
+
try { if (engine._ownSubagents && engine._ownSubagents.has(agent)) return next() } catch (e) {}
|
|
4467
4599
|
// 审查修复轮2:无 session 身份的 agent 不登记 runtime、不进 stateFor/refresh,直接放行
|
|
4468
4600
|
if (agent && engine.hasReliableSessionIdentity(agent)) {
|
|
4601
|
+
// 全局活动戳(pre-step 也是用户活动信号;暂离检测统揽全局)
|
|
4602
|
+
engine._globalLastActiveAt = Date.now()
|
|
4469
4603
|
// 首步即登记 runtime(重启恢复的会话也可能不触发 session-start,这里补登记)
|
|
4470
4604
|
const rt = engine.runtimeFor(agent)
|
|
4471
4605
|
// M7.5:pre-step 边界先冲刷 CoT 缓冲——保证本轮 context_push 的 window 携带最新思维链
|
|
@@ -4831,6 +4965,15 @@ export function apply(ctx, config) {
|
|
|
4831
4965
|
} catch (e) { return writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
|
|
4832
4966
|
},
|
|
4833
4967
|
},
|
|
4968
|
+
{
|
|
4969
|
+
kind: 'exact',
|
|
4970
|
+
path: API['semantic-deep-detect'],
|
|
4971
|
+
handler: async (req, res) => {
|
|
4972
|
+
// 打开即自动检测(0.1.37;GET 只读;loopback):快检+失败自动深扫+命中热接入+引导分流。
|
|
4973
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
4974
|
+
try { return writeJson(res, 200, await engine.semanticDeepDetect()) } catch (e) { return writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
|
|
4975
|
+
},
|
|
4976
|
+
},
|
|
4834
4977
|
{
|
|
4835
4978
|
kind: 'exact',
|
|
4836
4979
|
path: API['semantic-download'],
|
|
@@ -5581,6 +5724,12 @@ export function apply(ctx, config) {
|
|
|
5581
5724
|
void (async () => {
|
|
5582
5725
|
try {
|
|
5583
5726
|
for (const rt of engine.runtimes.values()) {
|
|
5727
|
+
// 过期丢弃(问题③):队列条目存活 30 分钟,过期不再重试(防配置性错误无限循环 spawn)
|
|
5728
|
+
while (rt.pendingConsolidations.length) {
|
|
5729
|
+
const it = rt.pendingConsolidations[0]
|
|
5730
|
+
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 }
|
|
5731
|
+
break
|
|
5732
|
+
}
|
|
5584
5733
|
if (rt.pendingConsolidations.length && !rt.consolidating) {
|
|
5585
5734
|
const item = rt.pendingConsolidations.shift()
|
|
5586
5735
|
if (item && item.agent) void engine.withAgent(item.agent, () => engine.consolidateTurn(item.turn, item.agent))
|
package/lib/semantic-js.js
CHANGED
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* 「要不要打断」仍属两车道策略(Python sidecar 在场时)。全部函数对非法输入 fail closed。
|
|
14
14
|
*/
|
|
15
15
|
import { createHash } from 'node:crypto'
|
|
16
|
-
import { existsSync, readFileSync, mkdirSync, renameSync, unlinkSync, writeFileSync, rmSync } from 'node:fs'
|
|
16
|
+
import { existsSync, readFileSync, mkdirSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync, rmSync } from 'node:fs'
|
|
17
17
|
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
18
|
+
import { createRequire } from 'node:module'
|
|
18
19
|
import path from 'node:path'
|
|
19
20
|
|
|
20
21
|
export const JS_SEMANTIC_ENGINE_VERSION = 'js_semantic_engine_v1'
|
|
@@ -74,14 +75,106 @@ function defaultModelsDirCandidates(pluginDir) {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
function defaultPeerDirCandidates(pluginDir) {
|
|
78
|
+
// pluginDir = <pkg>/lib(2026-09-02 issue 复核修正:此前上溯层级错位 → peerPresent 恒 false)。
|
|
79
|
+
// 静态候选仅作兜底,主路径是 resolvePeerTransformersDir 的真实解析:
|
|
80
|
+
// 1) lib/node_modules —— issue 临时 junction 绕过位(向后兼容)
|
|
81
|
+
// 2) <pkg>/node_modules —— 包内邻接位(lib 上 1 级再进 node_modules)
|
|
82
|
+
// 3) lib 上 3 级直拼 @huggingface —— 标准布局即 <root>/node_modules(pnpm hoisted/npm 提升),
|
|
83
|
+
// pnpm isolated 下即虚拟存储包内位 .pnpm/<hash>/node_modules
|
|
84
|
+
// 4) 开发树 artifacts
|
|
77
85
|
return [
|
|
78
86
|
path.join(pluginDir, 'node_modules', '@huggingface', 'transformers'),
|
|
79
|
-
|
|
80
|
-
path.join(pluginDir, '..', '..', '..', '
|
|
87
|
+
path.join(pluginDir, '..', 'node_modules', '@huggingface', 'transformers'),
|
|
88
|
+
path.join(pluginDir, '..', '..', '..', '@huggingface', 'transformers'),
|
|
81
89
|
path.join(pluginDir, '..', 'artifacts', 'm7-live-pre', 'js-semantic-trial', 'node_modules', '@huggingface', 'transformers'),
|
|
82
90
|
]
|
|
83
91
|
}
|
|
84
92
|
|
|
93
|
+
/**
|
|
94
|
+
* peer 探测(2026-09-02 issue 修正):真实解析优先 —— createRequire 从 lib/ 起走 Node 标准
|
|
95
|
+
* 解析,天然覆盖 npm/pnpm(hoisted/isolated 虚拟存储)/junction 全部布局,且不执行模块本体
|
|
96
|
+
* (require.resolve 只解析不加载);静态候选仅作 exports 损坏等极端情形的兜底;
|
|
97
|
+
* extraDirs = 深度扫描(semanticDeepDetect)热接入的额外目录,最后兜底检查。
|
|
98
|
+
* 返回命中的包目录,''=未找到。
|
|
99
|
+
*/
|
|
100
|
+
export function resolvePeerTransformersDir(pluginDir, extraDirs) {
|
|
101
|
+
try {
|
|
102
|
+
const req = createRequire(path.join(pluginDir, 'noop.js'))
|
|
103
|
+
const entry = req.resolve('@huggingface/transformers')
|
|
104
|
+
// entry 是包 main 文件(可能在任意子目录,如 dist/transformers.node.cjs):
|
|
105
|
+
// 从 entry 向上走到 package.json 所在层 = 包目录
|
|
106
|
+
if (entry && existsSync(entry)) {
|
|
107
|
+
let dir = path.dirname(entry)
|
|
108
|
+
while (dir && !existsSync(path.join(dir, 'package.json'))) {
|
|
109
|
+
const up = path.dirname(dir)
|
|
110
|
+
if (up === dir) { dir = ''; break }
|
|
111
|
+
dir = up
|
|
112
|
+
}
|
|
113
|
+
if (dir) return dir
|
|
114
|
+
}
|
|
115
|
+
} catch (_) { /* 静态候选兜底 */ }
|
|
116
|
+
const hit = defaultPeerDirCandidates(pluginDir).find((c) => existsSync(c))
|
|
117
|
+
if (hit) return hit
|
|
118
|
+
for (const d of Array.isArray(extraDirs) ? extraDirs : []) {
|
|
119
|
+
if (typeof d === 'string' && d && existsSync(d)) return d
|
|
120
|
+
}
|
|
121
|
+
return ''
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 深度扫描(0.1.37「打开即自动检测」;issue #14 后续):零依赖手扫常见安装根。
|
|
126
|
+
* 每个 root 探两层:root 自身的 node_modules(直传 profile 根/包根时命中)+ 一级子目录
|
|
127
|
+
* (直传 ~/.dsh/profiles 时枚举各 profile);每个落位再探 node_modules/@huggingface/
|
|
128
|
+
* transformers、pnpm 虚拟存储 .pnpm/node_modules/... 与 .pnpm/@huggingface+transformers@版本
|
|
129
|
+
* /node_modules/...(isolated 模式包体落位)。返回存在的包目录列表(去重,保持扫描序)。
|
|
130
|
+
*/
|
|
131
|
+
export function deepScanPeerTransformers(roots) {
|
|
132
|
+
const out = []
|
|
133
|
+
const seen = new Set()
|
|
134
|
+
const push = (p) => { if (p && !seen.has(p) && existsSync(p)) { seen.add(p); out.push(p) } }
|
|
135
|
+
const probeDir = (base) => {
|
|
136
|
+
if (!base || typeof base !== 'string') return
|
|
137
|
+
push(path.join(base, 'node_modules', '@huggingface', 'transformers'))
|
|
138
|
+
push(path.join(base, 'node_modules', '.pnpm', 'node_modules', '@huggingface', 'transformers'))
|
|
139
|
+
const pnpmDir = path.join(base, 'node_modules', '.pnpm')
|
|
140
|
+
let entries = []
|
|
141
|
+
try { entries = readdirSync(pnpmDir) } catch (_) { /* base 无 .pnpm 虚拟存储 */ }
|
|
142
|
+
for (const e of entries) {
|
|
143
|
+
if (/^@huggingface\+transformers@/.test(e)) push(path.join(pnpmDir, e, 'node_modules', '@huggingface', 'transformers'))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const root of Array.isArray(roots) ? roots : []) {
|
|
147
|
+
if (!root || typeof root !== 'string') continue
|
|
148
|
+
probeDir(root)
|
|
149
|
+
let children = []
|
|
150
|
+
try { children = readdirSync(root) } catch (_) { /* root 不可枚举 */ }
|
|
151
|
+
for (const c of children.slice(0, 64)) probeDir(path.join(root, c))
|
|
152
|
+
}
|
|
153
|
+
return out
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* semanticAssetProbe 共享实现(index.js 的 status API / 档位解析 / 引导卡同源):
|
|
158
|
+
* 模型资产=发行包 lib/models 优先,其次开发树;peer=resolvePeerTransformersDir。
|
|
159
|
+
* extraDirs 透传给 peer 探测(深度扫描热接入位)。返回形状与 v0.1.36 完全一致。
|
|
160
|
+
*/
|
|
161
|
+
export function probeJsSemanticAssets(pluginDir, extraDirs) {
|
|
162
|
+
const modelCands = [
|
|
163
|
+
path.join(pluginDir, 'models', E5_MODELS_SUBDIR, 'onnx', 'model_quantized.onnx'),
|
|
164
|
+
path.join(pluginDir, '..', 'artifacts', 'm7-live-pre', 'js-semantic-trial', 'models', E5_MODELS_SUBDIR, 'onnx', 'model_quantized.onnx'),
|
|
165
|
+
]
|
|
166
|
+
const modelOnnx = modelCands.find((c) => existsSync(c)) || modelCands[0]
|
|
167
|
+
const assetPresent = existsSync(modelOnnx)
|
|
168
|
+
const peerDir = resolvePeerTransformersDir(pluginDir, extraDirs)
|
|
169
|
+
return {
|
|
170
|
+
assetPresent,
|
|
171
|
+
peerPresent: Boolean(peerDir),
|
|
172
|
+
ready: Boolean(assetPresent && peerDir),
|
|
173
|
+
assetBytes: assetPresent ? statSync(modelOnnx).size : 0,
|
|
174
|
+
assetPath: modelOnnx,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
85
178
|
async function importPeerTransformers(peerDirs) {
|
|
86
179
|
try {
|
|
87
180
|
return await import('@huggingface/transformers') // 发行包:peer 邻接安装
|
|
@@ -213,6 +306,12 @@ export function createJsSemanticEnginePre(opts = {}) {
|
|
|
213
306
|
embedding: !!rebuilding,
|
|
214
307
|
}
|
|
215
308
|
},
|
|
309
|
+
/** 深度扫描热接入(0.1.37):把扫描发现的额外 peer 目录并入加载候选(去重,立即生效)。 */
|
|
310
|
+
addPeerDirCandidates(dirs) {
|
|
311
|
+
for (const d of Array.isArray(dirs) ? dirs : []) {
|
|
312
|
+
if (typeof d === 'string' && d && !peerDirCands.includes(d)) peerDirCands.push(d)
|
|
313
|
+
}
|
|
314
|
+
},
|
|
216
315
|
/**
|
|
217
316
|
* 对语料做稠密排名。返回 {miv, scores:Map(memoryId→cosine)};失败返回 null(调用方回退词法)。
|
|
218
317
|
*/
|
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.37",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|