@a9i5k4/dsh-auto-memory 2.2.5 → 2.2.6
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/client.js +59 -86
- package/lib/index.js +183 -2
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -561,6 +561,13 @@ window.__ModuleLoader__.load({
|
|
|
561
561
|
|
|
562
562
|
// ───────────────────────── 更新弹窗 / 首次指导 ─────────────────────────
|
|
563
563
|
var CHANGELOG = {
|
|
564
|
+
'2.2.6': { zh: [
|
|
565
|
+
'★ 修复:交接测量终于站到官方压缩的同一条边界上——官方自动压缩挂在每个 step 边界(pre-step,阈值 80%),而插件此前只在「一轮结束」测量;某一轮把水位从阈值下推到 80% 以上时,官方会在该轮就压缩完,交接白板/账本来不及写。现在 pre-step 也做同一测量(同一会话 5 秒节流),水位一到阈值就先写交接材料,不再被官方抢跑。',
|
|
566
|
+
'★ 自动接续改为宿主兜底:建新会话不再依赖浏览器页面——水位达标后在宿主侧开倒计时,确认卡照常弹出(同意/拒绝都直接发给宿主);页面被后台节流、标签页关闭或人不在时,倒计时一到宿主自己完成「刷新白板/账本 → 建新会话 → 沿用模型与工作区 → 注入交接材料」,不再因浏览器休眠而断档。',
|
|
567
|
+
], en: [
|
|
568
|
+
'★ Fix: handoff measurement now runs on the same boundary as official compaction — the official auto-compaction hook fires at every step boundary (pre-step, 80% threshold) while the plugin only measured at turn end; a single turn that pushes the level past 80% let the official compaction finish before the handoff ledger was written. The same measurement now also runs on pre-step (throttled to once per 5s per session), so the handoff material is written as soon as the threshold is crossed.',
|
|
569
|
+
'★ Auto-continue is now host-side: creating the new session no longer depends on the browser tab. Once the water level crosses the threshold the host starts a countdown; the confirm card still appears (Agree/Decline are sent straight to the host), and if the tab is throttled, closed, or nobody is at the keyboard, the host itself completes "refresh PLAN/ledger → create session → restore model & workspace → inject handoff material" when the countdown ends. No more missed handoffs from a sleeping browser.',
|
|
570
|
+
] },
|
|
564
571
|
'2.2.5': { zh: [
|
|
565
572
|
'★ 修复:上下文水位百分比不再被硬截断到 150%——旧版 `Math.min(tokens / window, 1.5)` 让任何超额都显示成同一个数(例如 761,692 / 131,072 真实为 581%)。现在如实上报,进度条仍按 100% 封顶。',
|
|
566
573
|
'★ 修复:水位卡标出的模型现在是「当前会话真实使用的模型」——此前一律读 settings.yaml 的 agent-default-model(新会话默认模型),你在界面切过模型后,卡片会张冠李戴地标成另一个模型(实测会话跑 deepseek-official/deepseek-v4.1-flash,却标成 opencode-go/deepseek-v4-flash)。现改为优先取会话日志 request/header 的 provider/model,默认模型只作兜底。',
|
|
@@ -854,6 +861,8 @@ window.__ModuleLoader__.load({
|
|
|
854
861
|
pyStatus: '/api/dsh-auto-memory/python-setup/status',
|
|
855
862
|
handoffState: '/api/dsh-auto-memory/handoff-state',
|
|
856
863
|
handoffContinue: '/api/dsh-auto-memory/handoff-continue',
|
|
864
|
+
autoContState: '/api/dsh-auto-memory/auto-continue-state',
|
|
865
|
+
autoContDecide: '/api/dsh-auto-memory/auto-continue-decide',
|
|
857
866
|
semanticDownload: '/api/dsh-auto-memory/semantic-download',
|
|
858
867
|
}
|
|
859
868
|
function query(params) {
|
|
@@ -3489,114 +3498,78 @@ window.__ModuleLoader__.load({
|
|
|
3489
3498
|
var cfP = useState(null)
|
|
3490
3499
|
var acConfirm = cfP[0]
|
|
3491
3500
|
var setAcConfirm = cfP[1]
|
|
3501
|
+
// 2.2.6:宿主兜底状态(倒计时/执行中/结果全部由宿主权威判定;浏览器只展示与传达决定)
|
|
3502
|
+
var asP = useState(null)
|
|
3503
|
+
var acState = asP[0]
|
|
3504
|
+
var setAcState = asP[1]
|
|
3492
3505
|
useEffect(function () {
|
|
3493
3506
|
var alive = true
|
|
3494
3507
|
var load = function () {
|
|
3495
3508
|
apiGet(API.config).then(function (c) { if (alive) setAcCfg(c || {}) }).catch(function () {})
|
|
3509
|
+
apiGet(API.autoContState).then(function (s) { if (alive && s) setAcState(s) }).catch(function () {})
|
|
3496
3510
|
}
|
|
3497
3511
|
load()
|
|
3498
|
-
var iv = setInterval(load,
|
|
3512
|
+
var iv = setInterval(load, 3000)
|
|
3499
3513
|
return function () { alive = false; clearInterval(iv) }
|
|
3500
3514
|
}, [])
|
|
3501
3515
|
useEffect(function () {
|
|
3516
|
+
var alive = true
|
|
3502
3517
|
var on = !(acCfg && acCfg.autoContinueEnabled === false)
|
|
3503
3518
|
if (!on) return
|
|
3504
3519
|
var thr = (acCfg && Number(acCfg.autoContinueThreshold)) || 0.75
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
var
|
|
3508
|
-
if (!(cooldownMin > 0)) cooldownMin = 30
|
|
3509
|
-
var alive = true
|
|
3510
|
-
var maybePrompt = function (edgeAt) {
|
|
3511
|
-
if (!alive || acBusy || autoState.cdIv || autoState.promptVisible) return
|
|
3512
|
-
if (!edgeAt || Date.now() - edgeAt > 10 * 60 * 1000) return // 边界太旧不再提示
|
|
3513
|
-
if (Date.now() < autoState.suppressUntil) return
|
|
3514
|
-
if (autoState.rejectedEdgeAt === edgeAt) return // ②拒绝过同一边界
|
|
3515
|
-
apiGet(API.handoffState, { sessionId: currentSessionIdClient() }).then(function (d) {
|
|
3516
|
-
if (!alive || !d || !d.enabled) return
|
|
3517
|
-
var wl = d.waterLevel || {}
|
|
3518
|
-
if (!wl.window || !(wl.ratio >= thr)) return
|
|
3519
|
-
var last = 0
|
|
3520
|
-
try { last = Number(localStorage.getItem('dsh-auto-memory.autoCont.last') || 0) } catch (eL) {}
|
|
3521
|
-
if (Date.now() - last < cooldownMin * 60 * 1000) return
|
|
3522
|
-
if (autoState.promptVisible || autoState.cdIv) return
|
|
3523
|
-
setAcConfirm({ ratio: Number(wl.ratio) || 0, tokens: Number(wl.tokens) || 0, window: Number(wl.window) || 0, edgeAt: edgeAt })
|
|
3524
|
-
setAcSt('')
|
|
3525
|
-
autoState.promptVisible = true
|
|
3526
|
-
var left = confirmSec
|
|
3527
|
-
setAcCd(left)
|
|
3528
|
-
autoState.cdIv = setInterval(function () {
|
|
3529
|
-
left -= 1
|
|
3530
|
-
if (left <= 0) {
|
|
3531
|
-
clearInterval(autoState.cdIv); autoState.cdIv = null; setAcCd(0)
|
|
3532
|
-
autoState.promptVisible = false; setAcConfirm(null)
|
|
3533
|
-
runAuto() // ③超时 = 挂机 → 自动接续兜底
|
|
3534
|
-
} else setAcCd(left)
|
|
3535
|
-
}, 1000)
|
|
3536
|
-
}).catch(function () {})
|
|
3537
|
-
}
|
|
3538
|
-
// 轮次边界:running true→false 边沿 + 3s 短确认
|
|
3539
|
-
var verifyEdge = function (edgeAt) {
|
|
3540
|
-
if (!alive || autoState.edgeAt !== edgeAt) return
|
|
3541
|
-
var info = currentRunningInfo()
|
|
3542
|
-
if (info && info.running) return // 短确认失败:新一轮已开始,不算边界
|
|
3543
|
-
maybePrompt(edgeAt)
|
|
3544
|
-
}
|
|
3545
|
-
var armEdge = function () {
|
|
3546
|
-
autoState.edgeAt = Date.now()
|
|
3547
|
-
var info = currentRunningInfo()
|
|
3548
|
-
autoState.edgeSession = info ? info.sessionId : ''
|
|
3549
|
-
var edgeAt = autoState.edgeAt
|
|
3550
|
-
setTimeout(function () { verifyEdge(edgeAt) }, 3000)
|
|
3551
|
-
}
|
|
3552
|
-
var observe = function () {
|
|
3520
|
+
// 渲染:armed 倒计时 → 确认卡;executing → 执行中;lastOk → 成功;error → 失败提示。
|
|
3521
|
+
// 触发/倒计时/超时执行全部由宿主侧驱动(2.2.6),浏览器即使被节流也不影响接续。
|
|
3522
|
+
var poll = function () {
|
|
3553
3523
|
if (!alive) return
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3524
|
+
apiGet(API.autoContState).then(function (s) {
|
|
3525
|
+
if (!alive || !s) return
|
|
3526
|
+
setAcState(s)
|
|
3527
|
+
if (s.executing) {
|
|
3528
|
+
setAcSt(locale === 'zh' ? '宿主正在自动接续:刷新白板/账本 → 建新会话 → 注入材料…' : 'Host auto-continuing: refresh PLAN/ledger → new session → inject…')
|
|
3529
|
+
return
|
|
3530
|
+
}
|
|
3531
|
+
if (s.lastOk) {
|
|
3532
|
+
setAcSt((locale === 'zh' ? '✓ 已自动接续到新会话,可直接上手' : '✓ Auto-continued into a new session') + (s.lastOk.model ? ' · ' + s.lastOk.model + (s.lastOk.reasoningEffort ? '/' + s.lastOk.reasoningEffort : '') : ''))
|
|
3533
|
+
return
|
|
3534
|
+
}
|
|
3535
|
+
if (s.error) { setAcSt('✗ ' + s.error); return }
|
|
3536
|
+
var arm = s.armed
|
|
3537
|
+
if (arm && arm.ratio >= thr) {
|
|
3538
|
+
setAcConfirm({ ratio: Number(arm.ratio) || 0, tokens: Number(arm.tokens) || 0, window: Number(arm.window) || 0, edgeAt: Number(arm.edgeAt) || 0 })
|
|
3539
|
+
setAcCd(Math.max(0, Math.ceil((Number(arm.expiresAt) || 0) - Date.now()) / 1000))
|
|
3540
|
+
} else {
|
|
3541
|
+
setAcConfirm(null)
|
|
3542
|
+
setAcCd(0)
|
|
3543
|
+
if (acSt && /^✗/.test(acSt)) setAcSt('')
|
|
3544
|
+
}
|
|
3545
|
+
}).catch(function () {})
|
|
3572
3546
|
}
|
|
3573
|
-
|
|
3547
|
+
poll()
|
|
3548
|
+
var iv = setInterval(poll, 3000)
|
|
3549
|
+
return function () { alive = false; clearInterval(iv) }
|
|
3550
|
+
}, [acCfg && acCfg.autoContinueEnabled === false, acCfg && (Number(acCfg.autoContinueThreshold) || 0.75)])
|
|
3574
3551
|
var cancelAc = function () {
|
|
3575
|
-
if (autoState.cdIv) { clearInterval(autoState.cdIv); autoState.cdIv = null }
|
|
3576
|
-
autoState.promptVisible = false
|
|
3577
3552
|
setAcCd(0)
|
|
3578
3553
|
}
|
|
3554
|
+
// 2.2.6:执行权在宿主。同意/拒绝都 POST 给宿主(浏览器只传达决定);执行/超时全部宿主侧完成。
|
|
3579
3555
|
var runAuto = function () {
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
setAcSt((locale === 'zh' ? '✓ 已自动接续到新会话,可直接上手' : '✓ Auto-continued into a new session') + (r && r.model ? ' · ' + r.model + (r.reasoningEffort ? '/' + r.reasoningEffort : '') : ''))
|
|
3591
|
-
setAcBusy(false)
|
|
3592
|
-
}).catch(function (e) { setAcSt('✗ ' + String(e && e.message ? e.message : e)); setAcBusy(false) })
|
|
3556
|
+
var edgeAt = (acConfirm && acConfirm.edgeAt) || 0
|
|
3557
|
+
apiPost(API.autoContDecide, { action: 'agree', edgeAt: edgeAt }).then(function (r) {
|
|
3558
|
+
if (r && r.ok) {
|
|
3559
|
+
setAcConfirm(null)
|
|
3560
|
+
setAcCd(0)
|
|
3561
|
+
setAcSt(locale === 'zh' ? '宿主正在自动接续:刷新白板/账本 → 建新会话 → 注入材料…' : 'Host auto-continuing: refresh PLAN/ledger → new session → inject…')
|
|
3562
|
+
} else {
|
|
3563
|
+
setAcSt('✗ ' + String((r && r.error) || 'auto-continue agree failed'))
|
|
3564
|
+
}
|
|
3565
|
+
}).catch(function (e) { setAcSt('✗ ' + String(e && e.message ? e.message : e)) })
|
|
3593
3566
|
}
|
|
3594
3567
|
var onAgree = function () { runAuto() }
|
|
3595
3568
|
var onReject = function () {
|
|
3596
|
-
var edgeAt = (acConfirm && acConfirm.edgeAt) ||
|
|
3569
|
+
var edgeAt = (acConfirm && acConfirm.edgeAt) || 0
|
|
3597
3570
|
cancelAc()
|
|
3598
3571
|
setAcConfirm(null)
|
|
3599
|
-
|
|
3572
|
+
apiPost(API.autoContDecide, { action: 'reject', edgeAt: edgeAt }).then(function () {}).catch(function () {})
|
|
3600
3573
|
setAcSt(t('autoContRejected'))
|
|
3601
3574
|
}
|
|
3602
3575
|
if (acConfirm || acCd > 0 || acSt) {
|
|
@@ -3609,7 +3582,7 @@ window.__ModuleLoader__.load({
|
|
|
3609
3582
|
h('button', { 'data-dam-btn': '', style: { marginLeft: '6px' }, onClick: onReject }, t('autoContReject')),
|
|
3610
3583
|
acCd > 0 ? h('span', { 'data-dam-hint': '', style: { marginLeft: '8px' } }, t('autoContTimeout').replace('{s}', String(acCd))) : null),
|
|
3611
3584
|
]) : null,
|
|
3612
|
-
!acConfirm && acCd > 0 ? h('div', null, t('autoContCountdown').replace('{s}', String(acCd)), ' ', h('button', { style: { marginLeft: '6px', border: '1px solid rgba(232,234,237,.45)', borderRadius: '8px', background: 'transparent', color: '#e8eaed', cursor: 'pointer', padding: '2px 10px' }, onClick:
|
|
3585
|
+
!acConfirm && acCd > 0 ? h('div', null, t('autoContCountdown').replace('{s}', String(acCd)), ' ', h('button', { style: { marginLeft: '6px', border: '1px solid rgba(232,234,237,.45)', borderRadius: '8px', background: 'transparent', color: '#e8eaed', cursor: 'pointer', padding: '2px 10px' }, onClick: onReject }, t('autoContCancel'))) : null,
|
|
3613
3586
|
acSt ? h('div', { style: (acConfirm || acCd > 0) ? { marginTop: '4px' } : {} }, acSt) : null)
|
|
3614
3587
|
}
|
|
3615
3588
|
return null
|
package/lib/index.js
CHANGED
|
@@ -161,6 +161,8 @@ export const API = {
|
|
|
161
161
|
'semantic-deep-detect': '/api/dsh-auto-memory/semantic-deep-detect',
|
|
162
162
|
'handoff-state': '/api/dsh-auto-memory/handoff-state',
|
|
163
163
|
'handoff-continue': '/api/dsh-auto-memory/handoff-continue',
|
|
164
|
+
'auto-continue-state': '/api/dsh-auto-memory/auto-continue-state',
|
|
165
|
+
'auto-continue-decide': '/api/dsh-auto-memory/auto-continue-decide',
|
|
164
166
|
'subagent-gc': '/api/dsh-auto-memory/subagent-gc',
|
|
165
167
|
'semantic-download': '/api/dsh-auto-memory/semantic-download',
|
|
166
168
|
'semantic-emit': '/api/dsh-auto-memory/semantic-emit',
|
|
@@ -1861,6 +1863,153 @@ class MemoryEngine {
|
|
|
1861
1863
|
} catch (e) { diag('checkWaterLevel error: ' + (e && e.message)) }
|
|
1862
1864
|
}
|
|
1863
1865
|
|
|
1866
|
+
/** M-CM6-A·水位 v3(2026-09-08):pre-step 边界补测 —— 官方自动压缩(compaction-basic)挂在 `agent/pre-step`,
|
|
1867
|
+
* 阈值 thresholdRatio=0.8 对「路由请求 token」;插件此前只在 turn-stopping 测量,一旦某一轮把水位从阈值下推到 0.8 以上,
|
|
1868
|
+
* 官方会在该轮的 pre-step 就压缩完,交接白板/账本根本来不及写(实测:20:11 轮末测得 768158/1000000=0.77 时,会话日志里
|
|
1869
|
+
* 已有 2 次 compaction/start+summary)。这里在 pre-step 也做同一测量,让交接与官方压缩站在同一条边界上。
|
|
1870
|
+
* 节流:同一 agent 至少间隔 minGapMs(默认 5s),避免每个 step 都扫会话事件/读 tokenMeter。 */
|
|
1871
|
+
checkWaterLevelAtStep(agent, minGapMs = 5000) {
|
|
1872
|
+
try {
|
|
1873
|
+
if (this.config.handoffEnabled === false) return
|
|
1874
|
+
if (!agent || !this.hasReliableSessionIdentity(agent)) return
|
|
1875
|
+
const rt = this.runtimeFor(agent)
|
|
1876
|
+
const now = Date.now()
|
|
1877
|
+
if (rt && rt.waterStepAt && now - rt.waterStepAt < minGapMs) return
|
|
1878
|
+
if (rt) rt.waterStepAt = now
|
|
1879
|
+
void this.checkWaterLevel(agent)
|
|
1880
|
+
} catch (e) { diag('checkWaterLevelAtStep error: ' + (e && e.message)) }
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
/** M-CM6-C·宿主兜底自动接续(2026-09-08,2.2.6):浏览器端 AutoContinueHost 被后台节流/关闭时,
|
|
1884
|
+
* 宿主自身在倒计时到期后直接建新会话。协议:turn-stopping 且水位≥阈值 → arm 倒计时(confirmSeconds,
|
|
1885
|
+
* 默认 35s,写入 _autoContState)→ 浏览器轮询 GET auto-continue-state 显示确认卡(同意=POST decide
|
|
1886
|
+
* agree,宿主立即执行;拒绝=POST decide reject,同一边界不再触发)→ 到期无人响应 → 宿主自动执行,
|
|
1887
|
+
* 与官方 SessionController service(ctx.get('sessionController'))同进程直调,不依赖浏览器。 */
|
|
1888
|
+
armAutoContinue(agent, wl) {
|
|
1889
|
+
try {
|
|
1890
|
+
if (!agent || !this.hasReliableSessionIdentity(agent)) return
|
|
1891
|
+
if (this.config.handoffEnabled === false) return
|
|
1892
|
+
if (this.config.autoContinueEnabled === false) return
|
|
1893
|
+
if (!wl || !(wl.ratio >= (Number(this.config.autoContinueThreshold) || 0.75))) return
|
|
1894
|
+
const st = this._autoContState || (this._autoContState = {})
|
|
1895
|
+
const now = Date.now()
|
|
1896
|
+
const cooldownMin = Number(this.config.autoContinueCooldownMinutes) || 30
|
|
1897
|
+
if (st.lastRunAt && now - st.lastRunAt < cooldownMin * 60000) return
|
|
1898
|
+
const sid = String(agent.session.id || agent.session.header?.id || '')
|
|
1899
|
+
// 拒绝语义:时间窗 10 分钟内不再 arm(近似客户端「拒绝=跳过本边界」;窗口过后按新边界重新评估)
|
|
1900
|
+
if (st.rejectedEdgeAt && now - st.rejectedEdgeAt < 10 * 60 * 1000) return
|
|
1901
|
+
if (st.armed && Date.now() < st.armed.expiresAt) return // 已有进行中的倒计时
|
|
1902
|
+
if (st.executing) return
|
|
1903
|
+
const confirmSec = Number(this.config.autoContinueConfirmSeconds)
|
|
1904
|
+
const sec = confirmSec >= 10 && confirmSec <= 120 ? confirmSec : 35
|
|
1905
|
+
st.armed = {
|
|
1906
|
+
edgeAt: now,
|
|
1907
|
+
sessionId: sid,
|
|
1908
|
+
expiresAt: now + sec * 1000,
|
|
1909
|
+
ratio: Number(wl.ratio) || 0,
|
|
1910
|
+
tokens: Number(wl.tokens) || 0,
|
|
1911
|
+
window: Number(wl.window) || 0,
|
|
1912
|
+
source: String(wl.source || ''),
|
|
1913
|
+
}
|
|
1914
|
+
diag('auto-continue armed: sid=' + sid + ' ratio=' + st.armed.ratio.toFixed(2) + ' expiresAt=' + st.armed.expiresAt)
|
|
1915
|
+
} catch (e) { diag('armAutoContinue error: ' + (e && e.message)) }
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
/** 心跳驱动:倒计时到期且无人决定 → 宿主自动执行接续。 */
|
|
1919
|
+
async tickAutoContinue() {
|
|
1920
|
+
try {
|
|
1921
|
+
const st = this._autoContState || (this._autoContState = {})
|
|
1922
|
+
if (!st.armed || st.executing) return
|
|
1923
|
+
if (Date.now() < st.armed.expiresAt) return
|
|
1924
|
+
diag('auto-continue deadline reached, executing host-side')
|
|
1925
|
+
await this.hostAutoContinue()
|
|
1926
|
+
} catch (e) { diag('tickAutoContinue error: ' + (e && e.message)) }
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
/** 宿主侧执行接续:刷账本(已由水位检查写过)→ 材料 → sessionController.create/selectModel/prompt。 */
|
|
1930
|
+
async hostAutoContinue() {
|
|
1931
|
+
const st = this._autoContState || (this._autoContState = {})
|
|
1932
|
+
if (st.executing) return { ok: false, error: 'already executing' }
|
|
1933
|
+
const armed = st.armed
|
|
1934
|
+
st.armed = null
|
|
1935
|
+
st.executing = true
|
|
1936
|
+
st.error = ''
|
|
1937
|
+
try {
|
|
1938
|
+
const sc = this._sessionController || (() => { try { this._sessionController = this._ctxRef?.get?.('sessionController') || undefined } catch (e) {} return this._sessionController })()
|
|
1939
|
+
if (!sc || typeof sc.create !== 'function') throw new Error('sessionController service unavailable(宿主无法建会话;请重启 dsh web 或保持页面打开)')
|
|
1940
|
+
const d = await this.buildContinueCarry()
|
|
1941
|
+
if (!d || !d.ok) throw new Error((d && d.error) || 'no handoff material')
|
|
1942
|
+
const created = await sc.create({
|
|
1943
|
+
...(d.workspaceId ? { workspaceId: d.workspaceId } : { cwd: d.ws }),
|
|
1944
|
+
...(d.agentPreset ? { agentPreset: d.agentPreset } : {}),
|
|
1945
|
+
})
|
|
1946
|
+
const newId = String((created && (created.sessionId || created.id)) || '')
|
|
1947
|
+
if (!newId) throw new Error('create returned no sessionId')
|
|
1948
|
+
if (d.provider && d.model && typeof sc.selectModel === 'function') {
|
|
1949
|
+
try { await sc.selectModel({ sessionId: newId, provider: d.provider, model: d.model, ...(d.reasoningEffort ? { reasoningEffort: d.reasoningEffort } : {}) }) } catch (eM) { diag('host auto-continue selectModel: ' + (eM && eM.message)) }
|
|
1950
|
+
}
|
|
1951
|
+
if (typeof sc.prompt === 'function') {
|
|
1952
|
+
const signal = typeof AbortSignal !== 'undefined' && AbortSignal.timeout ? AbortSignal.timeout(120000) : undefined
|
|
1953
|
+
await sc.prompt({ sessionId: newId, content: [{ type: 'text', text: d.carryText }], mode: 'queue' }, signal)
|
|
1954
|
+
}
|
|
1955
|
+
st.lastRunAt = Date.now()
|
|
1956
|
+
st.lastOk = { sessionId: newId, model: d.model || '', reasoningEffort: d.reasoningEffort || '', workspaceId: d.workspaceId || '' }
|
|
1957
|
+
diag('auto-continue host-executed: new session ' + newId + ' model=' + (d.model || 'default'))
|
|
1958
|
+
return { ok: true, sessionId: newId, model: d.model || '', reasoningEffort: d.reasoningEffort || '' }
|
|
1959
|
+
} catch (e) {
|
|
1960
|
+
st.error = String(e && e.message ? e.message : e)
|
|
1961
|
+
diag('hostAutoContinue error: ' + st.error)
|
|
1962
|
+
return { ok: false, error: st.error }
|
|
1963
|
+
} finally {
|
|
1964
|
+
st.executing = false
|
|
1965
|
+
st.armed = null
|
|
1966
|
+
}
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
/** 浏览器轮询视图:armed 倒计时 / 最近一次执行结果 / 拒绝边界 / 错误。 */
|
|
1970
|
+
autoContinueState() {
|
|
1971
|
+
const st = this._autoContState || (this._autoContState = {})
|
|
1972
|
+
const armed = st.armed ? {
|
|
1973
|
+
edgeAt: st.armed.edgeAt,
|
|
1974
|
+
sessionId: st.armed.sessionId,
|
|
1975
|
+
ratio: st.armed.ratio,
|
|
1976
|
+
tokens: st.armed.tokens,
|
|
1977
|
+
window: st.armed.window,
|
|
1978
|
+
expiresAt: st.armed.expiresAt,
|
|
1979
|
+
leftMs: Math.max(0, st.armed.expiresAt - Date.now()),
|
|
1980
|
+
} : null
|
|
1981
|
+
return {
|
|
1982
|
+
enabled: this.config.autoContinueEnabled !== false && this.config.handoffEnabled !== false,
|
|
1983
|
+
threshold: Number(this.config.autoContinueThreshold) || 0.75,
|
|
1984
|
+
armed,
|
|
1985
|
+
executing: !!st.executing,
|
|
1986
|
+
rejectedEdgeAt: st.rejectedEdgeAt || 0,
|
|
1987
|
+
lastRunAt: st.lastRunAt || 0,
|
|
1988
|
+
lastOk: st.lastOk || null,
|
|
1989
|
+
error: st.error || '',
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
/** 浏览器决定:agree=立即宿主执行;reject=标记该边界不再触发。 */
|
|
1994
|
+
async decideAutoContinue(action, edgeAt) {
|
|
1995
|
+
const st = this._autoContState || (this._autoContState = {})
|
|
1996
|
+
const armedEdge = st.armed && st.armed.edgeAt
|
|
1997
|
+
if (action === 'reject') {
|
|
1998
|
+
if (armedEdge !== undefined && armedEdge !== null) st.rejectedEdgeAt = armedEdge
|
|
1999
|
+
st.armed = null
|
|
2000
|
+
diag('auto-continue rejected at edge ' + String(armedEdge))
|
|
2001
|
+
return { ok: true, rejected: true }
|
|
2002
|
+
}
|
|
2003
|
+
if (action === 'agree') {
|
|
2004
|
+
if (st.executing) return { ok: false, error: 'already executing' }
|
|
2005
|
+
if (!st.armed) return { ok: false, error: 'no pending auto-continue' }
|
|
2006
|
+
edgeAt = edgeAt || st.armed.edgeAt
|
|
2007
|
+
if (edgeAt !== st.armed.edgeAt) return { ok: false, error: 'stale edge' }
|
|
2008
|
+
return this.hostAutoContinue()
|
|
2009
|
+
}
|
|
2010
|
+
return { ok: false, error: 'unknown action' }
|
|
2011
|
+
}
|
|
2012
|
+
|
|
1864
2013
|
/** 官方 token-meter 公式(dsh-context 移植版):文本 ≈ ceil(字符/4)+4 角色框定;忠实于 dsh-token-meter/estimate.ts 的文本分支。 */
|
|
1865
2014
|
estimateSessionTokens(messages) {
|
|
1866
2015
|
let tokens = 0
|
|
@@ -5868,7 +6017,13 @@ export function apply(ctx, config) {
|
|
|
5868
6017
|
// 最后活动时间写入该 agent 自己的 runtime(不再落到 currentRuntime()/default)
|
|
5869
6018
|
try { engine.runtimeFor(agent).lastActiveAt = Date.now() } catch (e) {}
|
|
5870
6019
|
// M-CM4 水位感知:每轮测量会话消息体量,越阈值→advisory+自动骨架账本(异步,不阻塞收尾)
|
|
5871
|
-
try { void engine.checkWaterLevel(agent)
|
|
6020
|
+
try { void engine.checkWaterLevel(agent).then(function () {
|
|
6021
|
+
// M-CM6-C 宿主兜底:轮末测量完成后若达标 → arm 倒计时(浏览器活着时由它展示确认卡;被节流/关闭则宿主到期自执行)
|
|
6022
|
+
try {
|
|
6023
|
+
const rt2 = engine.runtimeFor(agent)
|
|
6024
|
+
engine.armAutoContinue(agent, { ratio: rt2.waterLevel, tokens: rt2.waterLevelTokens, window: rt2.waterLevelWindow, source: rt2.waterLevelSource })
|
|
6025
|
+
} catch (eArm) {}
|
|
6026
|
+
}) } catch (e) {}
|
|
5872
6027
|
// 全局活动戳同步更新(暂离检测统揽全局,见 tickTime)
|
|
5873
6028
|
engine._globalLastActiveAt = Date.now()
|
|
5874
6029
|
try { engine.ingestAgentLifecycle(agent, 'agent/turn-stopping', { turn: payload && payload.turn, payload: { turn: payload && payload.turn } }) } catch (e) {}
|
|
@@ -5899,6 +6054,9 @@ export function apply(ctx, config) {
|
|
|
5899
6054
|
// M7.5:pre-step 边界先冲刷 CoT 缓冲——保证本轮 context_push 的 window 携带最新思维链
|
|
5900
6055
|
try { engine.flushReasoningBuffer(rt) } catch (_) {}
|
|
5901
6056
|
engine.ingestAgentLifecycle(agent, 'agent/pre-step', { turn: payload && payload.turn, step: payload && payload.step, payload: { turn: payload && payload.turn, step: payload && payload.step } })
|
|
6057
|
+
// M-CM6-A·水位 v3(2026-09-08):官方压缩在 pre-step 就动手,交接测量必须站同一条边界
|
|
6058
|
+
// (否则某轮把水位从 <阈值 推到 ≥0.8 时,官方先压缩、白板账本来不及写)。异步不阻塞放行。
|
|
6059
|
+
try { engine.checkWaterLevelAtStep(agent) } catch (eWL) {}
|
|
5902
6060
|
// M6-3:pre-step 时序(§8)——校验 cursor/index/TTL 后 claim packet,等待渲染面消费
|
|
5903
6061
|
try { if (engine._activationHost) engine._activationHost.onPreStep(agent) } catch (_) {}
|
|
5904
6062
|
// 只刷新顶层会话:子代理(自动沉淀/固化的 subagent)session 无 cwd,刷新会把 state 切到错误工作区
|
|
@@ -6366,6 +6524,29 @@ export function apply(ctx, config) {
|
|
|
6366
6524
|
} catch (e) { return writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
|
|
6367
6525
|
},
|
|
6368
6526
|
},
|
|
6527
|
+
{
|
|
6528
|
+
kind: 'exact',
|
|
6529
|
+
path: API['auto-continue-state'],
|
|
6530
|
+
handler: async (req, res) => {
|
|
6531
|
+
// M-CM6-C·宿主兜底状态(GET;loopback):浏览器 AutoContinueHost 轮询此端点展示确认卡/倒计时/执行结果。
|
|
6532
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
6533
|
+
try { return writeJson(res, 200, engine.autoContinueState()) } catch (e) { return writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
|
|
6534
|
+
},
|
|
6535
|
+
},
|
|
6536
|
+
{
|
|
6537
|
+
kind: 'exact',
|
|
6538
|
+
path: API['auto-continue-decide'],
|
|
6539
|
+
handler: async (req, res) => {
|
|
6540
|
+
// M-CM6-C·宿主兜底决定(POST;loopback):agree=宿主立即执行接续;reject=本边界不再触发。
|
|
6541
|
+
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
6542
|
+
if ((req.method || 'POST') !== 'POST') return writeJson(res, 405, { error: 'method not allowed' })
|
|
6543
|
+
try {
|
|
6544
|
+
const body = await readJsonBody(req).catch(() => ({}))
|
|
6545
|
+
const r = await engine.decideAutoContinue(body && body.action, body && body.edgeAt)
|
|
6546
|
+
return writeJson(res, r && r.ok ? 200 : 400, r || { ok: false, error: 'no result' })
|
|
6547
|
+
} catch (e) { return writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
|
|
6548
|
+
},
|
|
6549
|
+
},
|
|
6369
6550
|
{
|
|
6370
6551
|
kind: 'exact',
|
|
6371
6552
|
path: API['subagent-gc'],
|
|
@@ -7166,7 +7347,7 @@ export function apply(ctx, config) {
|
|
|
7166
7347
|
})()
|
|
7167
7348
|
}, 5 * 60 * 1000)
|
|
7168
7349
|
// 心跳独立 15 秒一次(对齐 polling-lease 心跳节奏),证明轮询机制活着
|
|
7169
|
-
const heartbeatTimer = setInterval(() => { void writeHeartbeat(); try { engine.tickTime() } catch (e) {}; try { void engine.subagentGcSweep() } catch (e) {} }, 15000)
|
|
7350
|
+
const heartbeatTimer = setInterval(() => { void writeHeartbeat(); try { engine.tickTime() } catch (e) {}; try { void engine.subagentGcSweep() } catch (e) {}; try { void engine.tickAutoContinue() } catch (e) {} }, 15000)
|
|
7170
7351
|
void writeHeartbeat() // 立即心跳一次:重启后马上可见轮询存活
|
|
7171
7352
|
|
|
7172
7353
|
// ---------- 注册与清理 ----------
|
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.
|
|
4
|
+
"version": "2.2.6",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|