@a9i5k4/dsh-auto-memory 2.4.0 → 2.4.1
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/OFFICIAL-DISCUSSION-DRAFT.md +80 -0
- package/docs/internal/SUBAGENT-REPORT-ROUTING-PRE-RESEARCH.md +261 -0
- package/lib/client.js +156 -84
- package/lib/index.js +98 -6
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1947,6 +1947,11 @@ class MemoryEngine {
|
|
|
1947
1947
|
const armRatio = hard ? Math.max(ratio, threshold) : ratio
|
|
1948
1948
|
rt.waterLevel = armRatio
|
|
1949
1949
|
rt.waterLevelTokens = estTokens
|
|
1950
|
+
// 2026-09-10:pre-step 的 arm 走的是 runtime 字段(见 checkWaterLevelAtStep → armAutoContinue),
|
|
1951
|
+
// 这里只写了 ratio/tokens、漏写 window/source,于是确认卡显示「512,311 / 0 token」(实测)。
|
|
1952
|
+
// 与 state 同源同值,一处漏写两处都缺 —— 必须成对维护。
|
|
1953
|
+
rt.waterLevelWindow = effectiveWin
|
|
1954
|
+
rt.waterLevelSource = winSource
|
|
1950
1955
|
this.state.waterLevelRatio = armRatio
|
|
1951
1956
|
this.state.waterLevelTokens = estTokens
|
|
1952
1957
|
this.state.waterLevelWindow = effectiveWin
|
|
@@ -1956,6 +1961,14 @@ class MemoryEngine {
|
|
|
1956
1961
|
this.state.waterLevelAt = Date.now()
|
|
1957
1962
|
this.state.waterLevelMeasuredTokens = estTokens
|
|
1958
1963
|
this.state.waterLevelReserve = reserve
|
|
1964
|
+
// 双口径(2026-09-10):官方小圈的分母是**声明窗口**(win),我们的分母是**可用额度**(effectiveWin),
|
|
1965
|
+
// 两者在本机差 384,000(预留输出)。用户看到「才 50% 就接续」正是因为只看到小圈那个数 ——
|
|
1966
|
+
// 把 ring 一并上报,界面就能同时显示「占可用额度 x% · 官方小圈 y%」,不再各说各话。
|
|
1967
|
+
this.state.waterLevelRing = (win > 0 && Number.isFinite(estTokens)) ? (estTokens / win) : 0
|
|
1968
|
+
// provider 自报的硬上限只有撞过墙(400 文本)才拿得到;拿得到就把「真实可写上限」也算出来,
|
|
1969
|
+
// 让界面能显示「距硬墙还剩多少 token」(本机 = 1,048,576 − 384,000 = 664,576)。
|
|
1970
|
+
const hardWin = Number(sig.overflow && sig.overflow.windowTokens) || 0
|
|
1971
|
+
this.state.waterLevelWall = hardWin > 0 ? Math.max(0, hardWin - reserve) : 0
|
|
1959
1972
|
this.state.waterLevelHardTrigger = hard ? (compacted ? 'compaction' : 'overflow') : ''
|
|
1960
1973
|
// 会话级记录:切会话时 /handoff-state?sessionId= 直接取这份,不再显示别的会话的数
|
|
1961
1974
|
this.rememberWaterRecord(agent && agent.session && agent.session.id ? String(agent.session.id) : '', {
|
|
@@ -2027,6 +2040,54 @@ class MemoryEngine {
|
|
|
2027
2040
|
} catch (e) { diag('checkWaterLevelAtStep error: ' + (e && e.message)) }
|
|
2028
2041
|
}
|
|
2029
2042
|
|
|
2043
|
+
/** 已接续闩锁(2026-09-10):同一个会话**只允许被接续一次**。
|
|
2044
|
+
* 旧实现只有 30 分钟冷却:冷却一过,用户切回那个旧会话、水位仍然高 → 再次 arm → 再次建会话,
|
|
2045
|
+
* 表现为「切回旧窗口,它还想接续」(实测用户报障)。闩锁以会话 id 为键、落盘到
|
|
2046
|
+
* ~/.dsh/memory/auto-continue-done.json(重启后依旧生效),接续**失败不落闩**(允许重试)。 */
|
|
2047
|
+
continuedSessionsFile() { return path.join(dshHome(), 'memory', 'auto-continue-done.json') }
|
|
2048
|
+
|
|
2049
|
+
loadContinuedSessions() {
|
|
2050
|
+
if (this._continuedSessions instanceof Set) return this._continuedSessions
|
|
2051
|
+
const set = new Set()
|
|
2052
|
+
try {
|
|
2053
|
+
const obj = JSON.parse(readFileSync(this.continuedSessionsFile(), 'utf8'))
|
|
2054
|
+
const arr = Array.isArray(obj) ? obj : ((obj && Array.isArray(obj.sessions)) ? obj.sessions : [])
|
|
2055
|
+
for (const it of arr) {
|
|
2056
|
+
const k = this.waterKey(typeof it === 'string' ? it : ((it && it.from) || ''))
|
|
2057
|
+
if (k) set.add(k)
|
|
2058
|
+
}
|
|
2059
|
+
} catch (e) {}
|
|
2060
|
+
this._continuedSessions = set
|
|
2061
|
+
return set
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
isContinuedSession(sid) {
|
|
2065
|
+
const k = this.waterKey(sid)
|
|
2066
|
+
return k ? this.loadContinuedSessions().has(k) : false
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
markContinuedSession(sid, toSid) {
|
|
2070
|
+
const k = this.waterKey(sid)
|
|
2071
|
+
if (!k) return false
|
|
2072
|
+
const set = this.loadContinuedSessions()
|
|
2073
|
+
if (set.has(k)) return false
|
|
2074
|
+
set.add(k)
|
|
2075
|
+
try {
|
|
2076
|
+
const file = this.continuedSessionsFile()
|
|
2077
|
+
mkdirSync(path.dirname(file), { recursive: true })
|
|
2078
|
+
let arr = []
|
|
2079
|
+
try {
|
|
2080
|
+
const obj = JSON.parse(readFileSync(file, 'utf8'))
|
|
2081
|
+
arr = Array.isArray(obj) ? obj : ((obj && Array.isArray(obj.sessions)) ? obj.sessions : [])
|
|
2082
|
+
} catch (e0) {}
|
|
2083
|
+
arr.push({ from: k, to: this.waterKey(toSid), at: Date.now() })
|
|
2084
|
+
if (arr.length > 200) arr = arr.slice(-200)
|
|
2085
|
+
writeFileSync(file, JSON.stringify({ sessions: arr, updatedAt: Date.now() }, null, 2), 'utf8')
|
|
2086
|
+
} catch (e) { diag('markContinuedSession write failed: ' + ((e && e.message) || e)) }
|
|
2087
|
+
diag('auto-continue latch: session ' + k + ' marked continued' + (toSid ? ' -> ' + this.waterKey(toSid) : ''))
|
|
2088
|
+
return true
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2030
2091
|
/** M-CM6-C·宿主兜底自动接续(2026-09-08,2.2.6):浏览器端 AutoContinueHost 被后台节流/关闭时,
|
|
2031
2092
|
* 宿主自身在倒计时到期后直接建新会话。协议:turn-stopping 且水位≥阈值 → arm 倒计时(confirmSeconds,
|
|
2032
2093
|
* 默认 35s,写入 _autoContState)→ 浏览器轮询 GET auto-continue-state 显示确认卡(同意=POST decide
|
|
@@ -2043,6 +2104,8 @@ class MemoryEngine {
|
|
|
2043
2104
|
const cooldownMin = Number(this.config.autoContinueCooldownMinutes) || 30
|
|
2044
2105
|
if (st.lastRunAt && now - st.lastRunAt < cooldownMin * 60000) return
|
|
2045
2106
|
const sid = String(agent.session.id || agent.session.header?.id || '')
|
|
2107
|
+
// 已接续闩锁(2026-09-10):该会话已经从它接续过一次 → 永远不再 arm(与 30 分钟冷却无关)
|
|
2108
|
+
if (this.isContinuedSession(sid)) return
|
|
2046
2109
|
// 拒绝语义:时间窗 10 分钟内不再 arm(近似客户端「拒绝=跳过本边界」;窗口过后按新边界重新评估)
|
|
2047
2110
|
if (st.rejectedEdgeAt && now - st.rejectedEdgeAt < 10 * 60 * 1000) return
|
|
2048
2111
|
if (st.armed && Date.now() < st.armed.expiresAt) return // 已有进行中的倒计时
|
|
@@ -2057,6 +2120,10 @@ class MemoryEngine {
|
|
|
2057
2120
|
tokens: Number(wl.tokens) || 0,
|
|
2058
2121
|
window: Number(wl.window) || 0,
|
|
2059
2122
|
source: String(wl.source || ''),
|
|
2123
|
+
// 双口径(2026-09-10):小圈读数(声明窗口为分母)与距硬墙剩余,供确认卡显示
|
|
2124
|
+
// (state 可能尚未建立 —— 例如测试夹具或极早的 arm,取不到就不显示,不得因此让 arm 失败)
|
|
2125
|
+
ring: Number(this.state && this.state.waterLevelRing) || 0,
|
|
2126
|
+
wall: Number(this.state && this.state.waterLevelWall) || 0,
|
|
2060
2127
|
// 2.2.7:pre-step 侧 arm 的倒计时到期时,若回合仍活跃则推迟(避免打断进行中的对话)
|
|
2061
2128
|
awaitIdle: !!(opts && opts.awaitIdle),
|
|
2062
2129
|
deferCount: 0,
|
|
@@ -2112,6 +2179,13 @@ class MemoryEngine {
|
|
|
2112
2179
|
})
|
|
2113
2180
|
const newId = String((created && (created.sessionId || created.id)) || '')
|
|
2114
2181
|
if (!newId) throw new Error('create returned no sessionId')
|
|
2182
|
+
// 接续序号标题(2026-09-10):浏览器路径一直有(「接续 #N · 工作区」),宿主兜底路径漏了 ——
|
|
2183
|
+
// 兜底接续出来的会话在侧栏是无名会话,用户无法一眼看出它是「延续会话」还是新开的
|
|
2184
|
+
// (实测 18:07 建出的 session-9cc01f76 日志里根本没有 session/title 事件)。
|
|
2185
|
+
if (d.contSeq && typeof sc.rename === 'function') {
|
|
2186
|
+
const title = '接续 #' + String(d.contSeq) + (d.wsBase ? ' · ' + d.wsBase : '')
|
|
2187
|
+
try { await sc.rename({ sessionId: newId, title }) } catch (eT) { diag('host auto-continue rename: ' + ((eT && eT.message) || eT)) }
|
|
2188
|
+
}
|
|
2115
2189
|
if (d.provider && d.model && typeof sc.selectModel === 'function') {
|
|
2116
2190
|
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
2191
|
}
|
|
@@ -2130,6 +2204,8 @@ class MemoryEngine {
|
|
|
2130
2204
|
// 旧会话优先按 id 从 agents 注册表取(_lastAgent 重启后可能为空),fail-soft 不影响接续结果。
|
|
2131
2205
|
const perm = await this.inheritPermissionForContinue(oldSid, newId)
|
|
2132
2206
|
const ritualNote = (ritual && ritual.ok) ? 'updated' : ((ritual && ritual.reason) || 'skipped')
|
|
2207
|
+
// 落闩:旧会话已接续过,此后不再被 arm(即使 30 分钟冷却已过)
|
|
2208
|
+
try { this.markContinuedSession(oldSid, newId) } catch (eL) {}
|
|
2133
2209
|
st.lastRunAt = Date.now()
|
|
2134
2210
|
st.lastOk = { sessionId: newId, model: d.model || '', reasoningEffort: d.reasoningEffort || '', workspaceId: d.workspaceId || '', permissionPreset: (perm && perm.ok && perm.preset) ? perm.preset : '', refreshRitual: ritualNote }
|
|
2135
2211
|
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 +2229,34 @@ class MemoryEngine {
|
|
|
2153
2229
|
}
|
|
2154
2230
|
}
|
|
2155
2231
|
|
|
2156
|
-
/** 浏览器轮询视图:armed 倒计时 / 最近一次执行结果 / 拒绝边界 / 错误。
|
|
2157
|
-
|
|
2232
|
+
/** 浏览器轮询视图:armed 倒计时 / 最近一次执行结果 / 拒绝边界 / 错误。
|
|
2233
|
+
* selfSid(2026-09-10):本状态是**全局单值**,而 armed 只属于某一个会话 —— 不过滤的话,
|
|
2234
|
+
* 别的会话的窗口也会弹「本会话水位已达 x%」(实测:用户切回旧窗口看到属于它的卡片后就以为清不掉)。
|
|
2235
|
+
* 传了 selfSid 且不匹配 → armed 视为 null(取不到 selfSid 时不过滤,fail-open,卡片照常显示)。 */
|
|
2236
|
+
autoContinueState(selfSid) {
|
|
2158
2237
|
const st = this._autoContState || (this._autoContState = {})
|
|
2159
|
-
const
|
|
2238
|
+
const selfKey = this.waterKey(selfSid || '')
|
|
2239
|
+
const armedRaw = st.armed ? {
|
|
2160
2240
|
edgeAt: st.armed.edgeAt,
|
|
2161
2241
|
sessionId: st.armed.sessionId,
|
|
2162
2242
|
ratio: st.armed.ratio,
|
|
2163
2243
|
tokens: st.armed.tokens,
|
|
2164
2244
|
window: st.armed.window,
|
|
2245
|
+
ring: Number(st.armed.ring) || 0,
|
|
2246
|
+
wall: Number(st.armed.wall) || 0,
|
|
2165
2247
|
expiresAt: st.armed.expiresAt,
|
|
2166
2248
|
leftMs: Math.max(0, st.armed.expiresAt - Date.now()),
|
|
2167
2249
|
} : null
|
|
2250
|
+
const armedMine = !selfKey || !armedRaw || !armedRaw.sessionId || this.waterKey(armedRaw.sessionId) === selfKey
|
|
2168
2251
|
return {
|
|
2169
2252
|
enabled: this.config.autoContinueEnabled !== false && this.config.handoffEnabled !== false,
|
|
2170
2253
|
threshold: Number(this.config.autoContinueThreshold) || 0.75,
|
|
2171
|
-
armed,
|
|
2254
|
+
armed: armedMine ? armedRaw : null,
|
|
2172
2255
|
executing: !!st.executing,
|
|
2173
2256
|
rejectedEdgeAt: st.rejectedEdgeAt || 0,
|
|
2174
2257
|
lastRunAt: st.lastRunAt || 0,
|
|
2175
|
-
|
|
2258
|
+
// at:客户端据此给「已完成」提示加 10 分钟有效期 —— 旧实现没有时间戳,提示会一直挂在所有窗口上
|
|
2259
|
+
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
2260
|
error: st.error || '',
|
|
2177
2261
|
}
|
|
2178
2262
|
}
|
|
@@ -7198,6 +7282,11 @@ export function apply(ctx, config) {
|
|
|
7198
7282
|
try {
|
|
7199
7283
|
const body = await readJsonBody(req).catch(() => ({}))
|
|
7200
7284
|
const r = await engine.inheritPermissionForContinue(body && body.fromSessionId, body && body.toSessionId, { attempts: (body && body.attempts) || undefined })
|
|
7285
|
+
// 落闩(2026-09-10):本端点由 client 在建好新会话后回调,带两个 sid → 正是「接续已发生」的事实点。
|
|
7286
|
+
// 放在这里而不是 handoff-continue(取材料,可能只是预览)是为了只在**真建成**时才关掉旧会话的自动接续。
|
|
7287
|
+
if (body && body.fromSessionId && body.toSessionId) {
|
|
7288
|
+
try { engine.markContinuedSession(body.fromSessionId, body.toSessionId) } catch (eL) {}
|
|
7289
|
+
}
|
|
7201
7290
|
return writeJson(res, 200, r || { ok: false, reason: 'no result' })
|
|
7202
7291
|
} catch (e) { return writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
|
|
7203
7292
|
},
|
|
@@ -7208,7 +7297,10 @@ export function apply(ctx, config) {
|
|
|
7208
7297
|
handler: async (req, res) => {
|
|
7209
7298
|
// M-CM6-C·宿主兜底状态(GET;loopback):浏览器 AutoContinueHost 轮询此端点展示确认卡/倒计时/执行结果。
|
|
7210
7299
|
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
7211
|
-
try {
|
|
7300
|
+
try {
|
|
7301
|
+
const url = new URL(req.url, 'http://127.0.0.1')
|
|
7302
|
+
return writeJson(res, 200, engine.autoContinueState(url.searchParams.get('sessionId')))
|
|
7303
|
+
} catch (e) { return writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
|
|
7212
7304
|
},
|
|
7213
7305
|
},
|
|
7214
7306
|
{
|
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.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|