@a9i5k4/dsh-auto-memory 2.4.0 → 2.4.2
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/docs/UI-INVENTORY-RAW.md +200 -0
- package/docs/UI-REFACTOR-PRE-RESEARCH.md +222 -0
- package/docs/internal/ART-DIRECTION-WIREFRAME.md +181 -0
- package/docs/internal/CUA-VISION-FIX-NOTES.md +78 -0
- package/docs/internal/DESIGN-OVERHAUL-PRE-RESEARCH.md +292 -0
- package/docs/internal/NEXT-VERSION-TODO.md +67 -0
- package/docs/internal/OFFICIAL-DISCUSSION-DRAFT.md +80 -0
- package/docs/internal/RELEASE-PROCESS.md +83 -0
- package/docs/internal/SUBAGENT-REPORT-ROUTING-PRE-RESEARCH.md +261 -0
- package/lib/client.js +163 -84
- package/lib/index.js +104 -8
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -266,8 +266,12 @@ const DEFAULT_CONFIG = {
|
|
|
266
266
|
waterLevelAdvisory: true,
|
|
267
267
|
/** 水位越阈时自动写一篇系统骨架账本(每会话一次;模型仍应自己写正式交接)。 */
|
|
268
268
|
waterLevelAutoHandoff: true,
|
|
269
|
-
/** 自动接续(
|
|
270
|
-
|
|
269
|
+
/** 自动接续(默认关):水位≥阈值 且 harness 权威 running==false(轮次边界,取代旧"两轮水位持平")时弹确认卡。 */
|
|
270
|
+
// 2026-09-10 改为默认关。原因:现有触发口径把「预留输出」从分母里扣掉(effectiveWin = win − reserve),
|
|
271
|
+
// 实测在本机路由下「上下文刚过半」就触发,与官方「约 80% 才压缩」相差约 45%,对公开用户是纯浪费。
|
|
272
|
+
// 待口径改为与官方压缩同坐标系后再评估是否翻回(见 docs/internal/NEXT-VERSION-TODO.md)。
|
|
273
|
+
// 注:老用户已落盘的显式值不受影响,只有新装用户吃这个默认。
|
|
274
|
+
autoContinueEnabled: false,
|
|
271
275
|
/** 自动接续水位阈值(0.5-0.95);2026-09-08 与 waterLevelThreshold 同步下调到 0.75(官方自动压缩阈值 80%,必须留余量)。 */
|
|
272
276
|
autoContinueThreshold: 0.75,
|
|
273
277
|
/** 确认卡倒计时秒数(30-40s 无操作 = 挂机 → 自动接续兜底)。 */
|
|
@@ -1947,6 +1951,11 @@ class MemoryEngine {
|
|
|
1947
1951
|
const armRatio = hard ? Math.max(ratio, threshold) : ratio
|
|
1948
1952
|
rt.waterLevel = armRatio
|
|
1949
1953
|
rt.waterLevelTokens = estTokens
|
|
1954
|
+
// 2026-09-10:pre-step 的 arm 走的是 runtime 字段(见 checkWaterLevelAtStep → armAutoContinue),
|
|
1955
|
+
// 这里只写了 ratio/tokens、漏写 window/source,于是确认卡显示「512,311 / 0 token」(实测)。
|
|
1956
|
+
// 与 state 同源同值,一处漏写两处都缺 —— 必须成对维护。
|
|
1957
|
+
rt.waterLevelWindow = effectiveWin
|
|
1958
|
+
rt.waterLevelSource = winSource
|
|
1950
1959
|
this.state.waterLevelRatio = armRatio
|
|
1951
1960
|
this.state.waterLevelTokens = estTokens
|
|
1952
1961
|
this.state.waterLevelWindow = effectiveWin
|
|
@@ -1956,6 +1965,14 @@ class MemoryEngine {
|
|
|
1956
1965
|
this.state.waterLevelAt = Date.now()
|
|
1957
1966
|
this.state.waterLevelMeasuredTokens = estTokens
|
|
1958
1967
|
this.state.waterLevelReserve = reserve
|
|
1968
|
+
// 双口径(2026-09-10):官方小圈的分母是**声明窗口**(win),我们的分母是**可用额度**(effectiveWin),
|
|
1969
|
+
// 两者在本机差 384,000(预留输出)。用户看到「才 50% 就接续」正是因为只看到小圈那个数 ——
|
|
1970
|
+
// 把 ring 一并上报,界面就能同时显示「占可用额度 x% · 官方小圈 y%」,不再各说各话。
|
|
1971
|
+
this.state.waterLevelRing = (win > 0 && Number.isFinite(estTokens)) ? (estTokens / win) : 0
|
|
1972
|
+
// provider 自报的硬上限只有撞过墙(400 文本)才拿得到;拿得到就把「真实可写上限」也算出来,
|
|
1973
|
+
// 让界面能显示「距硬墙还剩多少 token」(本机 = 1,048,576 − 384,000 = 664,576)。
|
|
1974
|
+
const hardWin = Number(sig.overflow && sig.overflow.windowTokens) || 0
|
|
1975
|
+
this.state.waterLevelWall = hardWin > 0 ? Math.max(0, hardWin - reserve) : 0
|
|
1959
1976
|
this.state.waterLevelHardTrigger = hard ? (compacted ? 'compaction' : 'overflow') : ''
|
|
1960
1977
|
// 会话级记录:切会话时 /handoff-state?sessionId= 直接取这份,不再显示别的会话的数
|
|
1961
1978
|
this.rememberWaterRecord(agent && agent.session && agent.session.id ? String(agent.session.id) : '', {
|
|
@@ -2027,6 +2044,54 @@ class MemoryEngine {
|
|
|
2027
2044
|
} catch (e) { diag('checkWaterLevelAtStep error: ' + (e && e.message)) }
|
|
2028
2045
|
}
|
|
2029
2046
|
|
|
2047
|
+
/** 已接续闩锁(2026-09-10):同一个会话**只允许被接续一次**。
|
|
2048
|
+
* 旧实现只有 30 分钟冷却:冷却一过,用户切回那个旧会话、水位仍然高 → 再次 arm → 再次建会话,
|
|
2049
|
+
* 表现为「切回旧窗口,它还想接续」(实测用户报障)。闩锁以会话 id 为键、落盘到
|
|
2050
|
+
* ~/.dsh/memory/auto-continue-done.json(重启后依旧生效),接续**失败不落闩**(允许重试)。 */
|
|
2051
|
+
continuedSessionsFile() { return path.join(dshHome(), 'memory', 'auto-continue-done.json') }
|
|
2052
|
+
|
|
2053
|
+
loadContinuedSessions() {
|
|
2054
|
+
if (this._continuedSessions instanceof Set) return this._continuedSessions
|
|
2055
|
+
const set = new Set()
|
|
2056
|
+
try {
|
|
2057
|
+
const obj = JSON.parse(readFileSync(this.continuedSessionsFile(), 'utf8'))
|
|
2058
|
+
const arr = Array.isArray(obj) ? obj : ((obj && Array.isArray(obj.sessions)) ? obj.sessions : [])
|
|
2059
|
+
for (const it of arr) {
|
|
2060
|
+
const k = this.waterKey(typeof it === 'string' ? it : ((it && it.from) || ''))
|
|
2061
|
+
if (k) set.add(k)
|
|
2062
|
+
}
|
|
2063
|
+
} catch (e) {}
|
|
2064
|
+
this._continuedSessions = set
|
|
2065
|
+
return set
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
isContinuedSession(sid) {
|
|
2069
|
+
const k = this.waterKey(sid)
|
|
2070
|
+
return k ? this.loadContinuedSessions().has(k) : false
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
markContinuedSession(sid, toSid) {
|
|
2074
|
+
const k = this.waterKey(sid)
|
|
2075
|
+
if (!k) return false
|
|
2076
|
+
const set = this.loadContinuedSessions()
|
|
2077
|
+
if (set.has(k)) return false
|
|
2078
|
+
set.add(k)
|
|
2079
|
+
try {
|
|
2080
|
+
const file = this.continuedSessionsFile()
|
|
2081
|
+
mkdirSync(path.dirname(file), { recursive: true })
|
|
2082
|
+
let arr = []
|
|
2083
|
+
try {
|
|
2084
|
+
const obj = JSON.parse(readFileSync(file, 'utf8'))
|
|
2085
|
+
arr = Array.isArray(obj) ? obj : ((obj && Array.isArray(obj.sessions)) ? obj.sessions : [])
|
|
2086
|
+
} catch (e0) {}
|
|
2087
|
+
arr.push({ from: k, to: this.waterKey(toSid), at: Date.now() })
|
|
2088
|
+
if (arr.length > 200) arr = arr.slice(-200)
|
|
2089
|
+
writeFileSync(file, JSON.stringify({ sessions: arr, updatedAt: Date.now() }, null, 2), 'utf8')
|
|
2090
|
+
} catch (e) { diag('markContinuedSession write failed: ' + ((e && e.message) || e)) }
|
|
2091
|
+
diag('auto-continue latch: session ' + k + ' marked continued' + (toSid ? ' -> ' + this.waterKey(toSid) : ''))
|
|
2092
|
+
return true
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2030
2095
|
/** M-CM6-C·宿主兜底自动接续(2026-09-08,2.2.6):浏览器端 AutoContinueHost 被后台节流/关闭时,
|
|
2031
2096
|
* 宿主自身在倒计时到期后直接建新会话。协议:turn-stopping 且水位≥阈值 → arm 倒计时(confirmSeconds,
|
|
2032
2097
|
* 默认 35s,写入 _autoContState)→ 浏览器轮询 GET auto-continue-state 显示确认卡(同意=POST decide
|
|
@@ -2043,6 +2108,8 @@ class MemoryEngine {
|
|
|
2043
2108
|
const cooldownMin = Number(this.config.autoContinueCooldownMinutes) || 30
|
|
2044
2109
|
if (st.lastRunAt && now - st.lastRunAt < cooldownMin * 60000) return
|
|
2045
2110
|
const sid = String(agent.session.id || agent.session.header?.id || '')
|
|
2111
|
+
// 已接续闩锁(2026-09-10):该会话已经从它接续过一次 → 永远不再 arm(与 30 分钟冷却无关)
|
|
2112
|
+
if (this.isContinuedSession(sid)) return
|
|
2046
2113
|
// 拒绝语义:时间窗 10 分钟内不再 arm(近似客户端「拒绝=跳过本边界」;窗口过后按新边界重新评估)
|
|
2047
2114
|
if (st.rejectedEdgeAt && now - st.rejectedEdgeAt < 10 * 60 * 1000) return
|
|
2048
2115
|
if (st.armed && Date.now() < st.armed.expiresAt) return // 已有进行中的倒计时
|
|
@@ -2057,6 +2124,10 @@ class MemoryEngine {
|
|
|
2057
2124
|
tokens: Number(wl.tokens) || 0,
|
|
2058
2125
|
window: Number(wl.window) || 0,
|
|
2059
2126
|
source: String(wl.source || ''),
|
|
2127
|
+
// 双口径(2026-09-10):小圈读数(声明窗口为分母)与距硬墙剩余,供确认卡显示
|
|
2128
|
+
// (state 可能尚未建立 —— 例如测试夹具或极早的 arm,取不到就不显示,不得因此让 arm 失败)
|
|
2129
|
+
ring: Number(this.state && this.state.waterLevelRing) || 0,
|
|
2130
|
+
wall: Number(this.state && this.state.waterLevelWall) || 0,
|
|
2060
2131
|
// 2.2.7:pre-step 侧 arm 的倒计时到期时,若回合仍活跃则推迟(避免打断进行中的对话)
|
|
2061
2132
|
awaitIdle: !!(opts && opts.awaitIdle),
|
|
2062
2133
|
deferCount: 0,
|
|
@@ -2112,6 +2183,13 @@ class MemoryEngine {
|
|
|
2112
2183
|
})
|
|
2113
2184
|
const newId = String((created && (created.sessionId || created.id)) || '')
|
|
2114
2185
|
if (!newId) throw new Error('create returned no sessionId')
|
|
2186
|
+
// 接续序号标题(2026-09-10):浏览器路径一直有(「接续 #N · 工作区」),宿主兜底路径漏了 ——
|
|
2187
|
+
// 兜底接续出来的会话在侧栏是无名会话,用户无法一眼看出它是「延续会话」还是新开的
|
|
2188
|
+
// (实测 18:07 建出的 session-9cc01f76 日志里根本没有 session/title 事件)。
|
|
2189
|
+
if (d.contSeq && typeof sc.rename === 'function') {
|
|
2190
|
+
const title = '接续 #' + String(d.contSeq) + (d.wsBase ? ' · ' + d.wsBase : '')
|
|
2191
|
+
try { await sc.rename({ sessionId: newId, title }) } catch (eT) { diag('host auto-continue rename: ' + ((eT && eT.message) || eT)) }
|
|
2192
|
+
}
|
|
2115
2193
|
if (d.provider && d.model && typeof sc.selectModel === 'function') {
|
|
2116
2194
|
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)) }
|
|
2117
2195
|
}
|
|
@@ -2130,6 +2208,8 @@ class MemoryEngine {
|
|
|
2130
2208
|
// 旧会话优先按 id 从 agents 注册表取(_lastAgent 重启后可能为空),fail-soft 不影响接续结果。
|
|
2131
2209
|
const perm = await this.inheritPermissionForContinue(oldSid, newId)
|
|
2132
2210
|
const ritualNote = (ritual && ritual.ok) ? 'updated' : ((ritual && ritual.reason) || 'skipped')
|
|
2211
|
+
// 落闩:旧会话已接续过,此后不再被 arm(即使 30 分钟冷却已过)
|
|
2212
|
+
try { this.markContinuedSession(oldSid, newId) } catch (eL) {}
|
|
2133
2213
|
st.lastRunAt = Date.now()
|
|
2134
2214
|
st.lastOk = { sessionId: newId, model: d.model || '', reasoningEffort: d.reasoningEffort || '', workspaceId: d.workspaceId || '', permissionPreset: (perm && perm.ok && perm.preset) ? perm.preset : '', refreshRitual: ritualNote }
|
|
2135
2215
|
diag('auto-continue host-executed: new session ' + newId + ' model=' + (d.model || 'default') + ' perm=' + ((perm && perm.ok && perm.preset) || ('(' + ((perm && perm.reason) || 'n/a') + ')')) + ' ritual=' + ritualNote)
|
|
@@ -2153,26 +2233,34 @@ class MemoryEngine {
|
|
|
2153
2233
|
}
|
|
2154
2234
|
}
|
|
2155
2235
|
|
|
2156
|
-
/** 浏览器轮询视图:armed 倒计时 / 最近一次执行结果 / 拒绝边界 / 错误。
|
|
2157
|
-
|
|
2236
|
+
/** 浏览器轮询视图:armed 倒计时 / 最近一次执行结果 / 拒绝边界 / 错误。
|
|
2237
|
+
* selfSid(2026-09-10):本状态是**全局单值**,而 armed 只属于某一个会话 —— 不过滤的话,
|
|
2238
|
+
* 别的会话的窗口也会弹「本会话水位已达 x%」(实测:用户切回旧窗口看到属于它的卡片后就以为清不掉)。
|
|
2239
|
+
* 传了 selfSid 且不匹配 → armed 视为 null(取不到 selfSid 时不过滤,fail-open,卡片照常显示)。 */
|
|
2240
|
+
autoContinueState(selfSid) {
|
|
2158
2241
|
const st = this._autoContState || (this._autoContState = {})
|
|
2159
|
-
const
|
|
2242
|
+
const selfKey = this.waterKey(selfSid || '')
|
|
2243
|
+
const armedRaw = st.armed ? {
|
|
2160
2244
|
edgeAt: st.armed.edgeAt,
|
|
2161
2245
|
sessionId: st.armed.sessionId,
|
|
2162
2246
|
ratio: st.armed.ratio,
|
|
2163
2247
|
tokens: st.armed.tokens,
|
|
2164
2248
|
window: st.armed.window,
|
|
2249
|
+
ring: Number(st.armed.ring) || 0,
|
|
2250
|
+
wall: Number(st.armed.wall) || 0,
|
|
2165
2251
|
expiresAt: st.armed.expiresAt,
|
|
2166
2252
|
leftMs: Math.max(0, st.armed.expiresAt - Date.now()),
|
|
2167
2253
|
} : null
|
|
2254
|
+
const armedMine = !selfKey || !armedRaw || !armedRaw.sessionId || this.waterKey(armedRaw.sessionId) === selfKey
|
|
2168
2255
|
return {
|
|
2169
2256
|
enabled: this.config.autoContinueEnabled !== false && this.config.handoffEnabled !== false,
|
|
2170
2257
|
threshold: Number(this.config.autoContinueThreshold) || 0.75,
|
|
2171
|
-
armed,
|
|
2258
|
+
armed: armedMine ? armedRaw : null,
|
|
2172
2259
|
executing: !!st.executing,
|
|
2173
2260
|
rejectedEdgeAt: st.rejectedEdgeAt || 0,
|
|
2174
2261
|
lastRunAt: st.lastRunAt || 0,
|
|
2175
|
-
|
|
2262
|
+
// at:客户端据此给「已完成」提示加 10 分钟有效期 —— 旧实现没有时间戳,提示会一直挂在所有窗口上
|
|
2263
|
+
lastOk: st.lastOk ? { sessionId: st.lastOk.sessionId, model: st.lastOk.model, reasoningEffort: st.lastOk.reasoningEffort, workspaceId: st.lastOk.workspaceId, permissionPreset: st.lastOk.permissionPreset, refreshRitual: st.lastOk.refreshRitual, at: st.lastRunAt || 0 } : null,
|
|
2176
2264
|
error: st.error || '',
|
|
2177
2265
|
}
|
|
2178
2266
|
}
|
|
@@ -7198,6 +7286,11 @@ export function apply(ctx, config) {
|
|
|
7198
7286
|
try {
|
|
7199
7287
|
const body = await readJsonBody(req).catch(() => ({}))
|
|
7200
7288
|
const r = await engine.inheritPermissionForContinue(body && body.fromSessionId, body && body.toSessionId, { attempts: (body && body.attempts) || undefined })
|
|
7289
|
+
// 落闩(2026-09-10):本端点由 client 在建好新会话后回调,带两个 sid → 正是「接续已发生」的事实点。
|
|
7290
|
+
// 放在这里而不是 handoff-continue(取材料,可能只是预览)是为了只在**真建成**时才关掉旧会话的自动接续。
|
|
7291
|
+
if (body && body.fromSessionId && body.toSessionId) {
|
|
7292
|
+
try { engine.markContinuedSession(body.fromSessionId, body.toSessionId) } catch (eL) {}
|
|
7293
|
+
}
|
|
7201
7294
|
return writeJson(res, 200, r || { ok: false, reason: 'no result' })
|
|
7202
7295
|
} catch (e) { return writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
|
|
7203
7296
|
},
|
|
@@ -7208,7 +7301,10 @@ export function apply(ctx, config) {
|
|
|
7208
7301
|
handler: async (req, res) => {
|
|
7209
7302
|
// M-CM6-C·宿主兜底状态(GET;loopback):浏览器 AutoContinueHost 轮询此端点展示确认卡/倒计时/执行结果。
|
|
7210
7303
|
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
7211
|
-
try {
|
|
7304
|
+
try {
|
|
7305
|
+
const url = new URL(req.url, 'http://127.0.0.1')
|
|
7306
|
+
return writeJson(res, 200, engine.autoContinueState(url.searchParams.get('sessionId')))
|
|
7307
|
+
} catch (e) { return writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
|
|
7212
7308
|
},
|
|
7213
7309
|
},
|
|
7214
7310
|
{
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a9i5k4/dsh-auto-memory",
|
|
3
3
|
"description": "Proactive associative memory for DSH: zero-prompt recall injected before the model speaks, three-layer auto-consolidation, skill crystallization, and Astra-style context management - handoff ledgers, PLAN whiteboard, water-level sensing. Local-first, model-agnostic, zero deps. 主动联想记忆+Astra 式上下文管理:自动唤回/自动沉淀/技能固化/交接账本与白板跨窗口续命/水位感知。",
|
|
4
|
-
"version": "2.4.
|
|
4
|
+
"version": "2.4.2",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|