@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.
Files changed (3) hide show
  1. package/lib/client.js +59 -86
  2. package/lib/index.js +183 -2
  3. 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, 30000)
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
- var confirmSec = Number(acCfg && acCfg.autoContinueConfirmSeconds)
3506
- if (!(confirmSec >= 10 && confirmSec <= 120)) confirmSec = 35
3507
- var cooldownMin = Number(acCfg && acCfg.autoContinueCooldownMinutes)
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
- var info = currentRunningInfo()
3555
- if (!info) return
3556
- if (autoState.lastRunning === null) {
3557
- autoState.lastRunning = info.running
3558
- if (!info.running) armEdge() // 首次观察即空闲(挂机/刚进会话):按边界处理
3559
- return
3560
- }
3561
- if (autoState.lastRunning === true && info.running === false) armEdge()
3562
- autoState.lastRunning = info.running
3563
- }
3564
- var unsub = null
3565
- try { if (sessions && sessions.list && typeof sessions.list.subscribe === 'function') unsub = sessions.list.subscribe(observe) } catch (eSub) {}
3566
- observe()
3567
- var iv = setInterval(function () { observe(); maybePrompt(autoState.edgeAt) }, 20000) // 兜底:水位后到 / 订阅不可用
3568
- return function () {
3569
- alive = false
3570
- clearInterval(iv)
3571
- if (unsub) { try { unsub() } catch (eU) {} }
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
- }, [acCfg && acCfg.autoContinueEnabled === false, acCfg && (Number(acCfg.autoContinueThreshold) || 0.75), acCfg && acCfg.autoContinueConfirmSeconds, acCfg && acCfg.autoContinueCooldownMinutes, acBusy])
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
- if (acBusy) return
3581
- cancelAc()
3582
- setAcConfirm(null)
3583
- if (!remoteFace || !remoteFace.session || typeof remoteFace.session.create !== 'function') {
3584
- setAcSt(locale === 'zh' ? '✗ remote.session 不可用——请重启 dsh web 后重试' : '✗ remote.session unavailable — restart dsh web')
3585
- return
3586
- }
3587
- setAcBusy(true)
3588
- setAcSt(locale === 'zh' ? '正在自动接续:刷新白板/账本 → 建新会话 → 注入材料…' : 'Auto-continuing: refresh PLAN/ledger → new session → inject…')
3589
- runContinueFlow({ onProgress: function (m) { setAcSt(m) } }).then(function (r) {
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) || autoState.edgeAt || 0
3569
+ var edgeAt = (acConfirm && acConfirm.edgeAt) || 0
3597
3570
  cancelAc()
3598
3571
  setAcConfirm(null)
3599
- autoState.rejectedEdgeAt = edgeAt
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: cancelAc }, t('autoContCancel'))) : null,
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) } catch (e) {}
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.5",
4
+ "version": "2.2.6",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {