@a9i5k4/dsh-auto-memory 2.2.4 → 2.2.5
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 +4 -2
- package/README.zh-CN.md +3 -1
- package/docs/USER-GUIDE.zh-CN.md +10 -4
- package/lib/client.js +93 -13
- package/lib/index.js +161 -56
- package/lib/water-window.js +32 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 无问自忆 · 记忆不断线
|
|
2
|
+
|
|
3
|
+
**dsh-auto-memory** — *She remembers, unbidden.*
|
|
2
4
|
|
|
3
5
|
> **EN** Now, across windows, too. Context that survives windows, sessions, and tools
|
|
4
|
-
> **中文**
|
|
6
|
+
> **中文** 该想起的,自己浮现。跨窗口 · 跨会话 · 跨工具,记忆不断线
|
|
5
7
|
|
|
6
8
|
<p align="center">
|
|
7
9
|
<a href="https://htmlpreview.github.io/?https://github.com/Aik358/dsh-auto-memory/blob/preview/docs/landing/index.html"><strong>🌐 Landing page (full feature tour · data flow · papers · screenshots)</strong></a>
|
package/README.zh-CN.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 无问自忆 · 记忆不断线
|
|
2
|
+
|
|
3
|
+
**dsh-auto-memory** — *She remembers, unbidden.*
|
|
2
4
|
|
|
3
5
|
> **中文** 现在,换窗口也不必。跨窗口 · 跨会话 · 跨工具,记忆不断线
|
|
4
6
|
> **EN** She remembers, unbidden — now, across windows, too. Context that survives windows, sessions, and tools
|
package/docs/USER-GUIDE.zh-CN.md
CHANGED
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
- **装完必须重启 dsh web**:插件的注入面(manifest)在启动时加载;改完 host 代码同理。
|
|
33
33
|
- 浏览器端更新后需**硬刷新**(Ctrl+Shift+R)才会加载新 client.js。
|
|
34
34
|
- 入口:左侧栏底部 **记忆** 按钮 → 记忆面板,含页签 **概览 / 白板 / 语料精修 / 设置**。
|
|
35
|
+
- 面板标题栏:**图钉**(线描图标,与 ⟳ ⤾ ✕ 同画风;点一下钉住后,点桌面其他地方不会自动收起,再点取消;钉住状态会记住)、⤾ 恢复默认位置、⟳ 刷新、✕ 关闭。未钉住时点击面板外或按 Esc 即收起。
|
|
36
|
+
- 面板标题里的版本号与 设置 →「检查更新」显示的都是**当前安装的版本**,与 npm 上的一致(发布时自动同步)。
|
|
35
37
|
- 数据全在本机:`~/.dsh/memory/`(记忆文件)、`~/.dsh/dsh-auto-memory-pre.json`(配置,发布版为 `dsh-auto-memory.json`)。
|
|
36
38
|
|
|
37
39
|
## 2. 第一次启动
|
|
@@ -132,7 +134,7 @@
|
|
|
132
134
|
| 白板注入预算(handoffPlanChars) | 1200 | 字符硬截断 |
|
|
133
135
|
| 账本注入预算(handoffLedgerChars) | 800 | 字符硬截断 |
|
|
134
136
|
| 水位窗口覆盖(waterLevelWindowTokens) | 0=自动 | 0 表示自动:优先官方路由容量,其次按当前模型查 settings.yaml。**除非特殊模型,保持 0** |
|
|
135
|
-
| 水位阈值(waterLevelThreshold) | 0.
|
|
137
|
+
| 水位阈值(waterLevelThreshold) | 0.75 | 越阈值即注入交接建议并自动补写账本。**默认 0.75 而非 0.8**:官方自动压缩阈值是 80%,阈值贴着 80% 常被官方先压缩掉,留 5% 余量(1M 窗口约 50K token)才来得及走完交接 |
|
|
136
138
|
| 水位建议注入(waterLevelAdvisory) | 开 | 无人值守时静默 |
|
|
137
139
|
| 越阈值自动写账本(waterLevelAutoHandoff) | 开 | 每会话一次,给 AI 一个骨架,正式账本仍由 AI 写 |
|
|
138
140
|
| **子代理痕迹回收**(subagentGcEnabled) | 开 | 见 §4.5 |
|
|
@@ -157,11 +159,15 @@
|
|
|
157
159
|
记忆面板 → 白板页签顶部显示:**已用 token / 窗口 token · 百分比**,后面标注两个来源:
|
|
158
160
|
|
|
159
161
|
- **计量**:`官方计量(usage)` = 与聊天框 context ring 同源(token-meter 的 `totalTokens`,即**当前上下文占用**);不可用时降级为启发式估算。
|
|
160
|
-
- **窗口**:`官方路由容量` = 取自会话日志 `request/context` 的 `contextWindow`(最权威);`自动检测: provider/model` =
|
|
162
|
+
- **窗口**:`官方路由容量` = 取自会话日志 `request/context` 的 `contextWindow`(最权威);`自动检测: provider/model` = 按**当前会话真实使用的模型**(会话日志 `request/header` 的 provider/model)查 `settings.yaml` 的 `contextWindow`,查不到才回退到 `agent-default-model`(新会话默认模型);`回退默认值` = 前两条都拿不到时的保守值(128K)——看到这个标签说明模型窗口没识别出来,可到设置页手动填 `waterLevelWindowTokens`。
|
|
163
|
+
> 你在这里看到的模型名是**这个会话正在用的模型**,不是设置里的默认模型;两者不一致时以会话为准(2026-09-08 修正,此前一律显示默认模型)。
|
|
164
|
+
> 卡片是**按会话**的:切换会话会自动换成该会话的数;该会话还没有实测数据(刚重启、或刚切过来还没发消息)时会标注「本会话尚未测量」,同时先把窗口与模型从该会话日志里推出来。
|
|
161
165
|
|
|
162
166
|
达到阈值后:AI 收到交接建议,并自动补写一篇骨架账本(正式账本仍由 AI 写)。
|
|
163
167
|
|
|
164
168
|
> **2.2.4 修复**:此前窗口解析有两条路同时失效(settings.yaml 的 flow 风格 YAML 解析不出、官方 contextWindow 事件只在会话开头出现而旧代码只扫最近 256 条),导致 1M 窗口被当成 128K、水位虚高显示 150%+。现已修复并如实标注来源。
|
|
169
|
+
>
|
|
170
|
+
> **比例如实上报**:旧版还把百分比硬截断到 150%,任何超额都显示成同一个数(例如 761,692 / 131,072 真实为 581%,却显示 150%)。现在显示真实百分比,进度条仍按 100% 封顶。注意:宿主代码不会热重载,**升级后必须重启 dsh web**(浏览器再 Ctrl+Shift+R),否则读到的仍是旧窗口解析。
|
|
165
171
|
|
|
166
172
|
### 4.2 交接白板
|
|
167
173
|
|
|
@@ -190,7 +196,7 @@
|
|
|
190
196
|
|
|
191
197
|
### 4.4 自动接续(免按钮,推荐)
|
|
192
198
|
|
|
193
|
-
白板页签 →「自动接续」卡片:开关(默认开)+ 阈值(默认 0.
|
|
199
|
+
白板页签 →「自动接续」卡片:开关(默认开)+ 阈值(默认 0.75,与水位阈值同步;官方自动压缩阈值是 80%,低于它才有意义)。
|
|
194
200
|
|
|
195
201
|
- **触发条件**:水位 ≥ 阈值 **且** harness 权威 `running` 位在轮次边界转为 `false`(会话真的空闲了)。长工具调用不会误触发。
|
|
196
202
|
- **确认卡三分支**:点「同意接续」= 立即接续;点「拒绝」= 本轮跳过,同一边界不再提示;**30–40 秒无操作** = 视为挂机,自动接续。
|
|
@@ -252,7 +258,7 @@ AI 在对话中可直接调用(你不需要记,但了解一下有好处):
|
|
|
252
258
|
|
|
253
259
|
| 现象 | 处理 |
|
|
254
260
|
|---|---|
|
|
255
|
-
| 水位显示 `xxx / 131,072 token · 150%` 之类 | 2.2.4
|
|
261
|
+
| 水位显示 `xxx / 131,072 token · 150%` 之类 | ①先确认**宿主已重启**(host 代码不热重载,只刷新页面没用)②窗口解析已在 2.2.4 修复;若标签仍是「回退默认值」,说明该模型不在 settings.yaml 里,手动填 `waterLevelWindowTokens` ③百分比已不再截断到 150%(旧版任何超额都显示 150%,真实值可能是 581%),进度条仍按 100% 封顶 |
|
|
256
262
|
| 提示「词法兜底 / 未就绪」 | 设置 → 自动记忆引擎 → ⟳ 检测,按引导下载 JS 模型或装 Python |
|
|
257
263
|
| 一键接续报「harness 未提供 remote.session」 | 重启 dsh web(注入面需重启加载);仍不行检查版本 ≥ 2.2.2 |
|
|
258
264
|
| 新会话落到「未分组工作区」/ 没沿用模型与思考档位 | 2.2.4 已修复(create 传 workspaceId、模型取自 request/header);升级后需**重启 dsh web + 硬刷新页面** |
|
package/lib/client.js
CHANGED
|
@@ -25,6 +25,10 @@ window.__ModuleLoader__.load({
|
|
|
25
25
|
var panelOpen = false
|
|
26
26
|
var panelClosing = false
|
|
27
27
|
var closeTimer = null
|
|
28
|
+
// 悬浮钉(2026-09-08):钉住后点击面板外不再自动收起,适合边看记忆边操作桌面
|
|
29
|
+
var PIN_KEY = 'dsh-auto-memory.panel.pinned'
|
|
30
|
+
var pinned = false
|
|
31
|
+
try { pinned = localStorage.getItem(PIN_KEY) === '1' } catch (ePin) {}
|
|
28
32
|
// 几何状态:left/top/width/height,持久化到 localStorage(用户可拖动/缩放)
|
|
29
33
|
var GEOM_KEY = 'dsh-auto-memory.panel.geom'
|
|
30
34
|
var DEFAULT_W = 440
|
|
@@ -133,6 +137,12 @@ window.__ModuleLoader__.load({
|
|
|
133
137
|
}, 170)
|
|
134
138
|
},
|
|
135
139
|
subscribe: function (fn) { listeners.add(fn); return function () { listeners.delete(fn) } },
|
|
140
|
+
isPinned: function () { return pinned },
|
|
141
|
+
togglePin: function () {
|
|
142
|
+
pinned = !pinned
|
|
143
|
+
try { localStorage.setItem(PIN_KEY, pinned ? '1' : '0') } catch (e) {}
|
|
144
|
+
emit()
|
|
145
|
+
},
|
|
136
146
|
}
|
|
137
147
|
|
|
138
148
|
// ───────────────────────── 国际化 ─────────────────────────
|
|
@@ -194,7 +204,7 @@ window.__ModuleLoader__.load({
|
|
|
194
204
|
kvMem: '记忆文件', kvMemUser: '用户', kvMemNotes: '笔记', kvMemLog: '日志', kvMemMissing: '缺失',
|
|
195
205
|
kvWs: '当前工作区', kvWsUnknown: '(未取到)', kvApi: 'API 连通', unknown: '(未知)',
|
|
196
206
|
updateTitle: 'dsh-auto-memory 已更新', updateSub: '本次更新内容:', gotIt: '知道了', noticeOpen: '了解更多',
|
|
197
|
-
refresh: '刷新', refreshing: '刷新中…', close: '关闭', dragMove: '拖动移动', dragResize: '拖动调整大小', resetPos: '恢复默认位置',
|
|
207
|
+
refresh: '刷新', refreshing: '刷新中…', close: '关闭', dragMove: '拖动移动', dragResize: '拖动调整大小', resetPos: '恢复默认位置', pinPanel: '钉住面板(点桌面其他地方不收起)', unpinPanel: '取消钉住(点面板外自动收起)',
|
|
198
208
|
generated: '已生成', failed: '失败: ',
|
|
199
209
|
generatedAt: '生成于 ',
|
|
200
210
|
pointsCount: ' 项', summarizing: 'AI 总结生成中…', summaryFailed: 'AI 总结生成失败,点 ⟳ 重试',
|
|
@@ -262,7 +272,7 @@ window.__ModuleLoader__.load({
|
|
|
262
272
|
fWaterWindow: '水位估计窗口(token)', fWaterWindowHint: '会话消息按官方公式(4 字符≈1 token)估算达到该值即视为上下文将满;0=关闭。默认 65536(128K 窗口的保守半量),按所用模型调整。',
|
|
263
273
|
fWaterAdvisory: '水位交接建议', fWaterAdvisoryHint: '水位越阈时在动态快照注入交接建议(写账本/刷新白板/建议开新窗);无人值守时静默。',
|
|
264
274
|
fWaterAuto: '水位自动骨架账本', fWaterAutoHint: '越阈时自动写一篇系统骨架账本(每会话一次),防止模型忽视建议时交接材料缺失。',
|
|
265
|
-
fWaterThreshold: '水位建议阈值', fWaterThresholdHint: '水位比例超过该值(0.1-1.5)即触发交接建议/骨架账本;默认 0.
|
|
275
|
+
fWaterThreshold: '水位建议阈值', fWaterThresholdHint: '水位比例超过该值(0.1-1.5)即触发交接建议/骨架账本;默认 0.75——官方自动压缩阈值是 80%,必须留出余量才来得及走完交接。',
|
|
266
276
|
fHandoffPlan: '白板注入预算(字符)', fHandoffPlanHint: 'PLAN.md 全貌注入动态快照的硬截断预算;全文可经 memory_read 或面板白板页查看。默认 1200。',
|
|
267
277
|
fHandoffLedger: '账本注入预算(字符)', fHandoffLedgerHint: '最新一篇交接账本注入动态快照的硬截断预算。默认 800。',
|
|
268
278
|
fConsSchedule: '定时做梦式固化', fConsScheduleHint: '每天到点自动读最近日志发散提炼长期要点,写入项目笔记/用户级记忆(等价 memory_consolidate)。',
|
|
@@ -270,7 +280,7 @@ window.__ModuleLoader__.load({
|
|
|
270
280
|
fConsScheduleDays: '固化回看天数', fConsScheduleDaysHint: '定时固化读取最近 N 天日志;默认 7。',
|
|
271
281
|
fMaintSchedule: '定时 30 天蒸馏', fMaintScheduleHint: '每天到点自动把超过 30 天的旧日志蒸馏归档(等价 memory_maintain);无旧日志时零成本跳过。',
|
|
272
282
|
fMaintScheduleTime: '蒸馏触发时间', fMaintScheduleTimeHint: 'HH:MM,默认 10:00(与固化时间错开)。',
|
|
273
|
-
waterCardTitle: '上下文水位', waterAutoSrc: '自动检测', waterManualSrc: '手动设定', waterOfficialSrc: '官方路由容量', waterFallbackSrc: '回退默认值', waterMeterOfficial: '官方计量', waterMeterHeuristic: '启发式降级', waterThresholdHint: '水位达到 {t} 时注入交接建议并自动补写账本;计量优先用官方 token-meter(与聊天框 context ring 同源),不可用时降级启发式估算;窗口优先取官方路由容量(request/context 的 contextWindow),其次按当前模型查 settings.yaml contextWindow,也可在下方手动覆盖。',
|
|
283
|
+
waterCardTitle: '上下文水位', waterAutoSrc: '自动检测', waterManualSrc: '手动设定', waterOfficialSrc: '官方路由容量', waterFallbackSrc: '回退默认值', waterNotMeasured: '本会话尚未测量(发送一轮后自动更新)', waterMeterOfficial: '官方计量', waterMeterHeuristic: '启发式降级', waterThresholdHint: '水位达到 {t} 时注入交接建议并自动补写账本;计量优先用官方 token-meter(与聊天框 context ring 同源),不可用时降级启发式估算;窗口优先取官方路由容量(request/context 的 contextWindow),其次按当前模型查 settings.yaml contextWindow,也可在下方手动覆盖。',
|
|
274
284
|
autoContTitle: '自动接续', autoContHint: '开启后:水位达到阈值、且旧会话已空闲(harness 权威 running 位在轮次边界转为 false,取代旧「两轮水位持平」)时,弹出确认卡——同意=立即接续,拒绝=本轮跳过(同一边界不再提示),30-40 秒无操作视为挂机自动接续;接续前先让旧 Agent 刷新白板 PLAN 与交接账本,再建新会话(沿用工作区/模型/思考档位)并注入分层交接材料;触发后默认 30 分钟内不重复。', autoContOn: '自动接续:开', autoContOff: '自动接续:关', autoContThreshold: '阈值', autoContCountdown: '⏳ {s} 秒后自动接续…', autoContCancel: '取消', autoContConfirm: '本会话水位已达 {p}%,旧会话已空闲(轮次边界)——现在接续到新会话?', autoContAgree: '同意接续', autoContReject: '拒绝', autoContTimeout: '{s} 秒后自动接续(视为挂机)', autoContRejected: '已跳过本次接续(下一个轮次边界再提醒)',
|
|
275
285
|
continueCardTitle: '一键接续', continueCardHint: '写好交接账本→创建新会话→把交接材料作为首条消息预载,并自动切换到新会话继续任务(接续材料含白板节选+最新账本;当前工作区无账本时自动取全局最近一篇)。', continueBtn: '一键接续到新会话', continueBusy: '处理中…',
|
|
276
286
|
fStyle: '反思风格', fStyleHint: '生活化 / 专业性 / 由内容决定。',
|
|
@@ -342,7 +352,7 @@ window.__ModuleLoader__.load({
|
|
|
342
352
|
kvMem: 'Memory files', kvMemUser: 'user', kvMemNotes: 'notes', kvMemLog: 'log', kvMemMissing: 'missing',
|
|
343
353
|
kvWs: 'Current workspace', kvWsUnknown: '(unknown)', kvApi: 'API probes', unknown: '(unknown)',
|
|
344
354
|
updateTitle: 'dsh-auto-memory Updated', updateSub: 'What\'s new in this update:', gotIt: 'Got it', noticeOpen: 'Learn more',
|
|
345
|
-
refresh: 'Refresh', refreshing: 'Refreshing…', close: 'Close', dragMove: 'Drag to move', dragResize: 'Drag to resize', resetPos: 'Reset position',
|
|
355
|
+
refresh: 'Refresh', refreshing: 'Refreshing…', close: 'Close', dragMove: 'Drag to move', dragResize: 'Drag to resize', resetPos: 'Reset position', pinPanel: 'Pin panel (stays open when clicking elsewhere)', unpinPanel: 'Unpin (auto-hide on outside click)',
|
|
346
356
|
generated: 'Generated', failed: 'Failed: ',
|
|
347
357
|
generatedAt: 'Generated ',
|
|
348
358
|
pointsCount: ' items', summarizing: 'AI summary generating…', summaryFailed: 'Summary failed, press refresh to retry',
|
|
@@ -410,7 +420,7 @@ window.__ModuleLoader__.load({
|
|
|
410
420
|
fWaterWindow: 'Water-level window (tokens)', fWaterWindowHint: 'Session messages priced with the official meter (4 chars ≈ 1 token); past this, context is considered nearly full; 0 disables. Default 65536 — a conservative half of a 128K window.',
|
|
411
421
|
fWaterAdvisory: 'Water-level handoff advisory', fWaterAdvisoryHint: 'Injects a handoff advisory past the threshold (write ledger / refresh plan / suggest a new window); silent in unattended mode.',
|
|
412
422
|
fWaterAuto: 'Auto skeleton ledger', fWaterAutoHint: 'Past the threshold, auto-writes one system skeleton ledger per session as a safety net if the model ignores the advisory.',
|
|
413
|
-
fWaterThreshold: 'Water-level threshold', fWaterThresholdHint: 'Inject the handoff advisory / skeleton ledger once the ratio passes this value (0.1-1.5); default 0.
|
|
423
|
+
fWaterThreshold: 'Water-level threshold', fWaterThresholdHint: 'Inject the handoff advisory / skeleton ledger once the ratio passes this value (0.1-1.5); default 0.75 — the official auto-compaction threshold is 80%, so a 5% margin is needed to finish the handoff first.',
|
|
414
424
|
fHandoffPlan: 'Plan injection budget (chars)', fHandoffPlanHint: 'Hard truncation budget for injecting the PLAN.md snapshot into the dynamic memory; full text via memory_read or the Whiteboard tab. Default 1200.',
|
|
415
425
|
fHandoffLedger: 'Ledger injection budget (chars)', fHandoffLedgerHint: 'Hard truncation budget for injecting the latest handoff ledger. Default 800.',
|
|
416
426
|
fConsSchedule: 'Scheduled deep consolidation', fConsScheduleHint: 'Once a day at the set time, read recent logs and distill long-term value into project notes / user memory (same flow as memory_consolidate).',
|
|
@@ -418,7 +428,7 @@ window.__ModuleLoader__.load({
|
|
|
418
428
|
fConsScheduleDays: 'Consolidation lookback (days)', fConsScheduleDaysHint: 'How many recent days of logs the scheduled consolidation reads; default 7.',
|
|
419
429
|
fMaintSchedule: 'Scheduled 30-day distill', fMaintScheduleHint: 'Once a day at the set time, distill & archive logs older than 30 days (same flow as memory_maintain); zero cost when nothing to archive.',
|
|
420
430
|
fMaintScheduleTime: 'Distill time', fMaintScheduleTimeHint: 'HH:MM, default 10:00 (staggered from the consolidation time).',
|
|
421
|
-
waterCardTitle: 'Context water level', waterAutoSrc: 'auto-detected', waterManualSrc: 'manual', waterOfficialSrc: 'route capacity', waterFallbackSrc: 'fallback default', waterMeterOfficial: 'official meter', waterMeterHeuristic: 'heuristic fallback', waterThresholdHint: 'Past {t} of the window, the handoff advisory is injected and the skeleton ledger is backfilled; metering prefers the official token-meter (same source as the chat context ring), falling back to the heuristic when unavailable; the window prefers the official route capacity (request/context contextWindow), then the active model in settings.yaml, or your manual override below.',
|
|
431
|
+
waterCardTitle: 'Context water level', waterAutoSrc: 'auto-detected', waterManualSrc: 'manual', waterOfficialSrc: 'route capacity', waterFallbackSrc: 'fallback default', waterNotMeasured: 'Not measured for this session yet (updates after the next turn)', waterMeterOfficial: 'official meter', waterMeterHeuristic: 'heuristic fallback', waterThresholdHint: 'Past {t} of the window, the handoff advisory is injected and the skeleton ledger is backfilled; metering prefers the official token-meter (same source as the chat context ring), falling back to the heuristic when unavailable; the window prefers the official route capacity (request/context contextWindow), then the active model in settings.yaml, or your manual override below.',
|
|
422
432
|
autoContTitle: 'Auto continue', autoContHint: 'When on: once the water level passes the threshold AND the old session is idle (the authoritative harness running bit flipped to false at a turn boundary, replacing the old "level unchanged across two polls"), a confirm card appears — Agree = continue now, Decline = skip this boundary (no re-prompt for it), no answer for 30-40s = unattended, continue automatically. Before continuing, the old agent refreshes PLAN + ledger; the new session keeps workspace/model/reasoning effort and receives layered handoff material; suppressed for 30 minutes after each run.', autoContOn: 'Auto continue: on', autoContOff: 'Auto continue: off', autoContThreshold: 'Threshold', autoContCountdown: '⏳ Auto-continue in {s}s…', autoContCancel: 'Cancel', autoContConfirm: 'Water level at {p}% and the old session is idle (turn boundary) — continue into a new session now?', autoContAgree: 'Continue', autoContReject: 'Decline', autoContTimeout: 'auto-continue in {s}s (unattended)', autoContRejected: 'Skipped this handoff (will ask again at the next turn boundary)',
|
|
423
433
|
continueCardTitle: 'One-click continue', continueCardHint: 'Writes the handoff ledger → creates a new session → injects the handoff material as the first message → switches to the new session automatically (material = PLAN excerpt + latest ledger; falls back to the most recent global ledger when this workspace has none).', continueBtn: 'Continue in a new session', continueBusy: 'Working…',
|
|
424
434
|
fStyle: 'Reflection style', fStyleHint: 'Life-style / Professional / Auto.',
|
|
@@ -492,6 +502,15 @@ window.__ModuleLoader__.load({
|
|
|
492
502
|
return s ? !!s.running : null
|
|
493
503
|
} catch (e) { return null }
|
|
494
504
|
}
|
|
505
|
+
// 当前会话 id(跟随 GUI 切换会话):水位卡按会话取数,切换即刷新(2026-09-08)
|
|
506
|
+
function currentSessionIdClient() {
|
|
507
|
+
try {
|
|
508
|
+
var snap = sessions && sessions.list && sessions.list.getSnapshot()
|
|
509
|
+
if (!snap) return ''
|
|
510
|
+
var id = snap.current
|
|
511
|
+
return (id === undefined || id === null) ? '' : String(id)
|
|
512
|
+
} catch (e) { return '' }
|
|
513
|
+
}
|
|
495
514
|
function amCtzValue() {
|
|
496
515
|
return (typeof Intl !== 'undefined' && typeof Intl.DateTimeFormat === 'function') ? (Intl.DateTimeFormat().resolvedOptions().timeZone || undefined) : undefined
|
|
497
516
|
}
|
|
@@ -542,6 +561,21 @@ window.__ModuleLoader__.load({
|
|
|
542
561
|
|
|
543
562
|
// ───────────────────────── 更新弹窗 / 首次指导 ─────────────────────────
|
|
544
563
|
var CHANGELOG = {
|
|
564
|
+
'2.2.5': { zh: [
|
|
565
|
+
'★ 修复:上下文水位百分比不再被硬截断到 150%——旧版 `Math.min(tokens / window, 1.5)` 让任何超额都显示成同一个数(例如 761,692 / 131,072 真实为 581%)。现在如实上报,进度条仍按 100% 封顶。',
|
|
566
|
+
'★ 修复:水位卡标出的模型现在是「当前会话真实使用的模型」——此前一律读 settings.yaml 的 agent-default-model(新会话默认模型),你在界面切过模型后,卡片会张冠李戴地标成另一个模型(实测会话跑 deepseek-official/deepseek-v4.1-flash,却标成 opencode-go/deepseek-v4-flash)。现改为优先取会话日志 request/header 的 provider/model,默认模型只作兜底。',
|
|
567
|
+
'★ 修复:水位卡改成按会话显示——此前是全机单值、只在有对话轮次时更新,切到别的会话仍显示上一个会话的数(或重启后的 0),模型名也只能拿默认模型凑。现在按 sessionId 取该会话的实测值;没有实测就现场从该会话日志推导窗口与模型,并明确标注「本会话尚未测量」。',
|
|
568
|
+
'★ 版本号同步:面板徽标与「检测更新」读的是开发树的 package.json,长期停在 0.1.30(而 npm 已是 2.2.x),于是界面显示旧版本、更新检查永远报「有新版本」。现在发布脚本会把版本号回写开发树,两条线永远一致。',
|
|
569
|
+
'★ 新增悬浮钉:记忆面板标题栏加了一个图钉按钮(线描图标,与 ⟳ ⤾ ✕ 同画风),点一下钉住——之后再点桌面或其他地方,面板不会自动收起(再点一下取消)。钉住状态会记住。',
|
|
570
|
+
'★ 交接阈值下调到 75%:官方自动压缩阈值是 80%,阈值贴着 80% 常常还没走完交接就被官方压缩掉;水位建议阈值与自动接续阈值默认都改为 0.75,留 5% 余量(1M 窗口约 50K token)。已存在的 0.8 配置不会被自动改写,可在设置页手动改。',
|
|
571
|
+
], en: [
|
|
572
|
+
'★ Fix: the context water-level percentage is no longer clamped to 150% — the old `Math.min(tokens / window, 1.5)` made every over-window reading look identical (761,692 / 131,072 is really 581%). It now reports the true value; the progress bar still caps at 100%.',
|
|
573
|
+
'★ Fix: the model shown on the water-level card is now the model the session is actually using — it used to always read agent-default-model from settings.yaml, so after switching models in the UI the card named a different model (observed: session running deepseek-official/deepseek-v4.1-flash but the card said opencode-go/deepseek-v4-flash). It now prefers provider/model from the session log\'s request/header, with the default model only as a fallback.',
|
|
574
|
+
'★ Fix: the water-level card is now per-session — it used to be a single global value updated only on turns, so switching conversations kept showing another session\'s numbers (or 0 after a restart) with the default model name. It now reads the measured value for the session id, and when there is none it derives the window/model from that session log and says "not measured for this session yet".',
|
|
575
|
+
'★ Version sync: the panel badge and "Check for updates" read the dev tree package.json, which sat at 0.1.30 while npm was already 2.2.x — the UI showed an old version and the update check always reported "update available". The release script now writes the version back to the dev tree so both lines stay in lockstep.',
|
|
576
|
+
'★ New pin button: the panel header now has a line-art pin toggle matching the ⟳ ⤾ ✕ icons — pin it and clicking elsewhere on the desktop no longer auto-hides the panel (click again to unpin). The pinned state is remembered.',
|
|
577
|
+
'★ Handoff threshold lowered to 75%: the official auto-compaction threshold is 80%, so a threshold sitting at 80% often gets compacted before the handoff finishes. Both the water-level advisory and the auto-continue threshold now default to 0.75, leaving 5% headroom (~50K tokens on a 1M window). An existing 0.8 in your config is not rewritten automatically — change it in Settings if you want the new default.',
|
|
578
|
+
] },
|
|
545
579
|
'2.2.4': { zh: [
|
|
546
580
|
'★ 修复:一键接续的新会话终于落进旧会话所属工作区——官方 session.create 只有传 workspaceId 才会绑定工作区,只传 cwd 仅设工作目录(故此前恒落「未分组」);现 host 先解析旧会话的 workspaceId 再建会话,解析不到才回退 cwd。',
|
|
547
581
|
'★ 修复:新会话沿用旧会话的模型与思考档位——模型信息在会话日志的 request/header.header.config(旧版查错了事件类型,恒取空 → 回落默认模型)。',
|
|
@@ -876,6 +910,7 @@ window.__ModuleLoader__.load({
|
|
|
876
910
|
'[data-dam-resize]:hover { opacity: 1; }',
|
|
877
911
|
'[data-dam-btn] { border: none; background: transparent; cursor: pointer; color: inherit; opacity: .75; font-size: calc(13px * var(--dam-scale)); padding: 4px 8px; border-radius: 6px; }',
|
|
878
912
|
'[data-dam-btn]:hover { opacity: 1; background: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 16%, transparent); }',
|
|
913
|
+
'[data-dam-btn][data-pin="on"] { opacity: 1; background: color-mix(in srgb, var(--dam-accent, #4f7cff) 24%, transparent); }',
|
|
879
914
|
'[data-dam-tabs-wrap] { display: flex; align-items: center; min-width: 0; border-bottom: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.2)) 55%, transparent); }',
|
|
880
915
|
'[data-dam-tabs] { flex: 1; min-width: 0; overflow: hidden; padding: 5px 4px; scrollbar-width: none; -ms-overflow-style: none; }',
|
|
881
916
|
'[data-dam-tabs]::-webkit-scrollbar { display: none; }',
|
|
@@ -1178,6 +1213,22 @@ window.__ModuleLoader__.load({
|
|
|
1178
1213
|
var label = props && props.label ? props.label : t('loading')
|
|
1179
1214
|
return h('div', { 'data-dam-loading': '' }, h('span', { 'data-dam-spinner': '', 'aria-hidden': 'true' }), h('span', null, label))
|
|
1180
1215
|
}
|
|
1216
|
+
/** 线描图钉图标(2026-09-08):与标题栏 ⤾ ⟳ ✕ 同一画风——单色 currentColor 描边、无彩色 emoji;
|
|
1217
|
+
* 未钉住=描边空心,已钉住=填充(内孔用 evenodd 挖空)。 */
|
|
1218
|
+
function PinIcon(props) {
|
|
1219
|
+
var filled = !!(props && props.filled)
|
|
1220
|
+
return h('svg', {
|
|
1221
|
+
width: 13, height: 13, viewBox: '0 0 16 16', 'aria-hidden': 'true',
|
|
1222
|
+
style: { display: 'block', margin: '0 auto', pointerEvents: 'none' },
|
|
1223
|
+
}, h('path', {
|
|
1224
|
+
d: 'M8 1.7c2.2 0 4 1.8 4 4 0 2.9-4 8.6-4 8.6S4 8.6 4 5.7c0-2.2 1.8-4 4-4zM8 4.2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z',
|
|
1225
|
+
fill: filled ? 'currentColor' : 'none',
|
|
1226
|
+
fillRule: 'evenodd',
|
|
1227
|
+
stroke: 'currentColor',
|
|
1228
|
+
strokeWidth: '1.1',
|
|
1229
|
+
strokeLinejoin: 'round',
|
|
1230
|
+
}))
|
|
1231
|
+
}
|
|
1181
1232
|
function AnimatedDisclosure(props) {
|
|
1182
1233
|
var open = !!props.open
|
|
1183
1234
|
var shownPair = useState(open)
|
|
@@ -2012,6 +2063,10 @@ window.__ModuleLoader__.load({
|
|
|
2012
2063
|
var dataPair = useState(null)
|
|
2013
2064
|
var data = dataPair[0]
|
|
2014
2065
|
var setData = dataPair[1]
|
|
2066
|
+
// 当前会话 id:水位卡按会话取数(切换会话即刷新,2026-09-08)
|
|
2067
|
+
var sidPair = useState(function () { return currentSessionIdClient() })
|
|
2068
|
+
var sid = sidPair[0]
|
|
2069
|
+
var setSid = sidPair[1]
|
|
2015
2070
|
var openPair = useState(null)
|
|
2016
2071
|
var open = openPair[0]
|
|
2017
2072
|
var setOpen = openPair[1]
|
|
@@ -2031,7 +2086,7 @@ window.__ModuleLoader__.load({
|
|
|
2031
2086
|
var setAutoCfg = autoPair[1]
|
|
2032
2087
|
useEffect(function () {
|
|
2033
2088
|
apiGet(API.config).then(function (c) {
|
|
2034
|
-
setAutoCfg({ enabled: !(c && c.autoContinueEnabled === false), threshold: (c && Number(c.autoContinueThreshold)) || 0.
|
|
2089
|
+
setAutoCfg({ enabled: !(c && c.autoContinueEnabled === false), threshold: (c && Number(c.autoContinueThreshold)) || 0.75 })
|
|
2035
2090
|
}).catch(function () { setAutoCfg({ enabled: false, threshold: 0.7 }) })
|
|
2036
2091
|
}, [])
|
|
2037
2092
|
var autoSave = function (patch) {
|
|
@@ -2051,8 +2106,22 @@ window.__ModuleLoader__.load({
|
|
|
2051
2106
|
}
|
|
2052
2107
|
useEffect(function () {
|
|
2053
2108
|
var alive = true
|
|
2054
|
-
|
|
2109
|
+
setData(null)
|
|
2110
|
+
apiGet(API.handoffState, { sessionId: sid }).then(function (d) { if (alive) setData(d) }).catch(function () {})
|
|
2055
2111
|
return function () { alive = false }
|
|
2112
|
+
}, [sid])
|
|
2113
|
+
// 切换会话即刷新水位卡:订阅官方会话列表快照,current 变化时重取(2026-09-08)
|
|
2114
|
+
useEffect(function () {
|
|
2115
|
+
var unsub = null
|
|
2116
|
+
try {
|
|
2117
|
+
if (sessions && sessions.list && typeof sessions.list.subscribe === 'function') {
|
|
2118
|
+
unsub = sessions.list.subscribe(function () {
|
|
2119
|
+
var now = currentSessionIdClient()
|
|
2120
|
+
setSid(function (prev) { return prev === now ? prev : now })
|
|
2121
|
+
})
|
|
2122
|
+
}
|
|
2123
|
+
} catch (eSub) {}
|
|
2124
|
+
return function () { if (typeof unsub === 'function') unsub() }
|
|
2056
2125
|
}, [])
|
|
2057
2126
|
useEffect(function () {
|
|
2058
2127
|
if (!open) return
|
|
@@ -2087,9 +2156,11 @@ window.__ModuleLoader__.load({
|
|
|
2087
2156
|
(wl.tokens || 0).toLocaleString() + ' / ' + wl.window.toLocaleString() + ' token · ' + pct + '%',
|
|
2088
2157
|
' ', h('span', { 'data-dam-hint': '' }, srcLabel),
|
|
2089
2158
|
' ', h('span', { 'data-dam-hint': '' }, wl.meter ? (wl.meter.indexOf('official') === 0 ? '· ' + t('waterMeterOfficial') + '(' + String(wl.meter).slice(9) + ')' : '· ' + t('waterMeterHeuristic')) : '')),
|
|
2159
|
+
// 本会话尚未测量(刚重启/刚切过来还没发消息):明说,而不是显示成 0 让人以为上下文是空的
|
|
2160
|
+
wl.at ? null : h('div', { 'data-dam-hint': '', style: { marginBottom: '4px' } }, t('waterNotMeasured')),
|
|
2090
2161
|
h('div', { style: { height: '6px', borderRadius: '3px', background: 'color-mix(in srgb, currentColor 12%, transparent)', overflow: 'hidden' } },
|
|
2091
2162
|
h('div', { style: { height: '100%', width: Math.min(pct, 100) + '%', background: barColor, borderRadius: '3px' } })),
|
|
2092
|
-
h('div', { 'data-dam-hint': '', style: { marginTop: '4px' } }, t('waterThresholdHint').replace('{t}', String(wl.threshold || 0.
|
|
2163
|
+
h('div', { 'data-dam-hint': '', style: { marginTop: '4px' } }, t('waterThresholdHint').replace('{t}', String(wl.threshold || 0.75))),
|
|
2093
2164
|
])))
|
|
2094
2165
|
}
|
|
2095
2166
|
if (autoCfg && wl.window > 0) {
|
|
@@ -2812,6 +2883,14 @@ window.__ModuleLoader__.load({
|
|
|
2812
2883
|
h('strong', null, t('autoMemory')),
|
|
2813
2884
|
h('span', { 'data-dam-hint': '', style: { fontSize: '10px', opacity: .55 } }, ver ? 'v' + ver + '-ui' : ''),
|
|
2814
2885
|
h('span', { className: 'dam-spacer' }),
|
|
2886
|
+
// 悬浮钉(2026-09-08):钉住后点面板外不再自动收起;图标为线描 SVG,与 ⤾ ⟳ ✕ 同画风
|
|
2887
|
+
h('button', {
|
|
2888
|
+
'data-dam-btn': '', 'data-pin': pinned ? 'on' : 'off',
|
|
2889
|
+
title: pinned ? t('unpinPanel') : t('pinPanel'),
|
|
2890
|
+
'aria-pressed': pinned ? 'true' : 'false',
|
|
2891
|
+
style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: '4px 6px' },
|
|
2892
|
+
onClick: function () { controller.togglePin() },
|
|
2893
|
+
}, h(PinIcon, { filled: pinned })),
|
|
2815
2894
|
h('button', { 'data-dam-btn': '', title: t('resetPos'), onClick: function () { controller.resetGeom() } }, '⤾'),
|
|
2816
2895
|
h('button', { 'data-dam-btn': '', title: t('refresh'), onClick: function () { setNonce(nonce + 1) } }, '⟳'),
|
|
2817
2896
|
h('button', { 'data-dam-btn': '', title: t('close'), onClick: function () { controller.close() } }, '✕')),
|
|
@@ -3422,7 +3501,7 @@ window.__ModuleLoader__.load({
|
|
|
3422
3501
|
useEffect(function () {
|
|
3423
3502
|
var on = !(acCfg && acCfg.autoContinueEnabled === false)
|
|
3424
3503
|
if (!on) return
|
|
3425
|
-
var thr = (acCfg && Number(acCfg.autoContinueThreshold)) || 0.
|
|
3504
|
+
var thr = (acCfg && Number(acCfg.autoContinueThreshold)) || 0.75
|
|
3426
3505
|
var confirmSec = Number(acCfg && acCfg.autoContinueConfirmSeconds)
|
|
3427
3506
|
if (!(confirmSec >= 10 && confirmSec <= 120)) confirmSec = 35
|
|
3428
3507
|
var cooldownMin = Number(acCfg && acCfg.autoContinueCooldownMinutes)
|
|
@@ -3433,7 +3512,7 @@ window.__ModuleLoader__.load({
|
|
|
3433
3512
|
if (!edgeAt || Date.now() - edgeAt > 10 * 60 * 1000) return // 边界太旧不再提示
|
|
3434
3513
|
if (Date.now() < autoState.suppressUntil) return
|
|
3435
3514
|
if (autoState.rejectedEdgeAt === edgeAt) return // ②拒绝过同一边界
|
|
3436
|
-
apiGet(API.handoffState).then(function (d) {
|
|
3515
|
+
apiGet(API.handoffState, { sessionId: currentSessionIdClient() }).then(function (d) {
|
|
3437
3516
|
if (!alive || !d || !d.enabled) return
|
|
3438
3517
|
var wl = d.waterLevel || {}
|
|
3439
3518
|
if (!wl.window || !(wl.ratio >= thr)) return
|
|
@@ -3491,7 +3570,7 @@ window.__ModuleLoader__.load({
|
|
|
3491
3570
|
clearInterval(iv)
|
|
3492
3571
|
if (unsub) { try { unsub() } catch (eU) {} }
|
|
3493
3572
|
}
|
|
3494
|
-
}, [acCfg && acCfg.autoContinueEnabled === false, acCfg && (Number(acCfg.autoContinueThreshold) || 0.
|
|
3573
|
+
}, [acCfg && acCfg.autoContinueEnabled === false, acCfg && (Number(acCfg.autoContinueThreshold) || 0.75), acCfg && acCfg.autoContinueConfirmSeconds, acCfg && acCfg.autoContinueCooldownMinutes, acBusy])
|
|
3495
3574
|
var cancelAc = function () {
|
|
3496
3575
|
if (autoState.cdIv) { clearInterval(autoState.cdIv); autoState.cdIv = null }
|
|
3497
3576
|
autoState.promptVisible = false
|
|
@@ -4060,7 +4139,7 @@ window.__ModuleLoader__.load({
|
|
|
4060
4139
|
field(t('fHandoffPlan'), h('input', { 'data-dam-input': '', type: 'number', min: 200, value: cfg.handoffPlanChars === undefined ? 1200 : cfg.handoffPlanChars, onChange: function (e) { set('handoffPlanChars', Number(e.target.value) || 1200) } }), t('fHandoffPlanHint')),
|
|
4061
4140
|
field(t('fHandoffLedger'), h('input', { 'data-dam-input': '', type: 'number', min: 100, value: cfg.handoffLedgerChars === undefined ? 800 : cfg.handoffLedgerChars, onChange: function (e) { set('handoffLedgerChars', Number(e.target.value) || 800) } }), t('fHandoffLedgerHint')),
|
|
4062
4141
|
field(t('fWaterWindow'), h('input', { 'data-dam-input': '', type: 'number', min: 0, value: cfg.waterLevelWindowTokens === undefined ? 65536 : cfg.waterLevelWindowTokens, onChange: function (e) { set('waterLevelWindowTokens', Number(e.target.value) || 0) } }), t('fWaterWindowHint')),
|
|
4063
|
-
field(t('fWaterThreshold'), h('input', { 'data-dam-input': '', type: 'number', min: 0.1, max: 1.5, step: 0.05, value: cfg.waterLevelThreshold === undefined ? 0.
|
|
4142
|
+
field(t('fWaterThreshold'), h('input', { 'data-dam-input': '', type: 'number', min: 0.1, max: 1.5, step: 0.05, value: cfg.waterLevelThreshold === undefined ? 0.75 : cfg.waterLevelThreshold, onChange: function (e) { set('waterLevelThreshold', Number(e.target.value) || 0.75) } }), t('fWaterThresholdHint')),
|
|
4064
4143
|
field(t('fWaterAdvisory'), h('input', { type: 'checkbox', checked: cfg.waterLevelAdvisory !== false, onChange: function (e) { set('waterLevelAdvisory', e.target.checked) } }), t('fWaterAdvisoryHint')),
|
|
4065
4144
|
field(t('fWaterAuto'), h('input', { type: 'checkbox', checked: cfg.waterLevelAutoHandoff !== false, onChange: function (e) { set('waterLevelAutoHandoff', e.target.checked) } }), t('fWaterAutoHint')),
|
|
4066
4145
|
field(locale === 'zh' ? '子代理痕迹回收' : 'Subagent trace recycle', h('input', { type: 'checkbox', checked: cfg.subagentGcEnabled !== false, onChange: function (e) { set('subagentGcEnabled', e.target.checked) } }),
|
|
@@ -4105,6 +4184,7 @@ window.__ModuleLoader__.load({
|
|
|
4105
4184
|
try {
|
|
4106
4185
|
var onDocPointerDown = function (e) {
|
|
4107
4186
|
if (!panelOpen || panelClosing) return
|
|
4187
|
+
if (controller.isPinned()) return // 悬浮钉:钉住后点面板外不收起(2026-09-08)
|
|
4108
4188
|
var el = e.target
|
|
4109
4189
|
if (el && el.closest && (el.closest('[data-dam-panel]') || el.closest('[data-dam-sidebar-btn]'))) return
|
|
4110
4190
|
controller.close()
|
package/lib/index.js
CHANGED
|
@@ -43,8 +43,8 @@ import { createMemoryHubPre } from './memory-hub.js'
|
|
|
43
43
|
import { pickConsolidationTextPre } from './intent-clean.js'
|
|
44
44
|
import { createStorageManagerPre } from './storage-manage.js'
|
|
45
45
|
import { rankWorkspacesByMemoryRecencyPre } from './ws-overview-rank.js'
|
|
46
|
-
import { scanPluginSubagentSessions, recycleSessions, PLUGIN_LABEL_PREFIX } from './subagent-gc.js'
|
|
47
|
-
import { parseModelWindowsPre, pickWindowPre, findOfficialContextWindowPre } from './water-window.js'
|
|
46
|
+
import { scanPluginSubagentSessions, recycleSessions, PLUGIN_LABEL_PREFIX, decodeZstdFrames } from './subagent-gc.js'
|
|
47
|
+
import { parseModelWindowsPre, pickWindowPre, findOfficialContextWindowPre, findSessionModelPre } from './water-window.js'
|
|
48
48
|
import { buildSourceCatalog, CorpusRegistry, canonicalize } from './m4-corpus.js'
|
|
49
49
|
import * as nodeZlib from 'node:zlib'
|
|
50
50
|
import { AsyncLocalStorage } from 'node:async_hooks'
|
|
@@ -221,16 +221,18 @@ const DEFAULT_CONFIG = {
|
|
|
221
221
|
handoffLedgerChars: 800,
|
|
222
222
|
/** M-CM4 水位感知:窗口 token 数。0=自动——从 settings.yaml 的 agent-default-model 解析对应模型的 contextWindow(失败回退 131072);手动设值即覆盖自动检测。 */
|
|
223
223
|
waterLevelWindowTokens: 0,
|
|
224
|
-
/** 水位建议阈值(0.1-1.5):ratio 越阈值时注入交接建议。
|
|
225
|
-
|
|
224
|
+
/** 水位建议阈值(0.1-1.5):ratio 越阈值时注入交接建议。
|
|
225
|
+
* 2026-09-08 由 0.8 下调到 0.75:harness 官方自动压缩阈值是 80%,阈值贴着 80% 会在交接动作完成前先被官方压缩掉,
|
|
226
|
+
* 留 5% 余量(约 1M 窗口的 50K token)才来得及走完「写账本 → 建新会话 → 注入材料」。 */
|
|
227
|
+
waterLevelThreshold: 0.75,
|
|
226
228
|
/** 水位建议注入(无人值守时始终静默)。 */
|
|
227
229
|
waterLevelAdvisory: true,
|
|
228
230
|
/** 水位越阈时自动写一篇系统骨架账本(每会话一次;模型仍应自己写正式交接)。 */
|
|
229
231
|
waterLevelAutoHandoff: true,
|
|
230
232
|
/** 自动接续(默认开):水位≥阈值 且 harness 权威 running==false(轮次边界,取代旧"两轮水位持平")时弹确认卡。 */
|
|
231
233
|
autoContinueEnabled: true,
|
|
232
|
-
/** 自动接续水位阈值(0.5-0.95)。 */
|
|
233
|
-
autoContinueThreshold: 0.
|
|
234
|
+
/** 自动接续水位阈值(0.5-0.95);2026-09-08 与 waterLevelThreshold 同步下调到 0.75(官方自动压缩阈值 80%,必须留余量)。 */
|
|
235
|
+
autoContinueThreshold: 0.75,
|
|
234
236
|
/** 确认卡倒计时秒数(30-40s 无操作 = 挂机 → 自动接续兜底)。 */
|
|
235
237
|
autoContinueConfirmSeconds: 35,
|
|
236
238
|
/** 接续前刷新仪式(默认开):先让旧 Agent 刷新 PLAN.md + 交接账本,host 再用最新材料组装交接。 */
|
|
@@ -1643,16 +1645,20 @@ class MemoryEngine {
|
|
|
1643
1645
|
}
|
|
1644
1646
|
|
|
1645
1647
|
/** M-CM4·自动窗口:agent-default-model(provider+model)→ settings.yaml 对应模型的 contextWindow;失败回退保守值。60s 缓存。 */
|
|
1646
|
-
|
|
1648
|
+
/** M-CM4·自动窗口:会话真实模型(request/header 的 provider/model,优先)→ agent-default-model(兜底)→ settings.yaml 对应模型的 contextWindow;失败回退保守值。60s 缓存。 */
|
|
1649
|
+
async resolveWaterWindow(providerOverride = '', modelOverride = '') {
|
|
1647
1650
|
try {
|
|
1648
1651
|
const override = Number(this.config.waterLevelWindowTokens) || 0
|
|
1649
1652
|
if (override > 0) return { window: override, source: 'manual' }
|
|
1650
1653
|
const now = Date.now()
|
|
1651
|
-
|
|
1654
|
+
const cacheKey = String(providerOverride || '') + '/' + String(modelOverride || '')
|
|
1655
|
+
if (this._waterWindowCache && now - this._waterWindowCache.at < 60000 && this._waterWindowCache.key === cacheKey) return this._waterWindowCache.value
|
|
1652
1656
|
const text = await this.readTextSafe(path.join(dshHome(), 'settings.yaml'))
|
|
1653
1657
|
let value = 0
|
|
1654
|
-
let model = ''
|
|
1655
|
-
let provider = ''
|
|
1658
|
+
let model = String(modelOverride || '')
|
|
1659
|
+
let provider = String(providerOverride || '')
|
|
1660
|
+
let defModel = ''
|
|
1661
|
+
let defProvider = ''
|
|
1656
1662
|
if (text) {
|
|
1657
1663
|
const lines = text.split(/\r?\n/)
|
|
1658
1664
|
const ai = lines.findIndex((l) => /^agent-default-model:/.test(l))
|
|
@@ -1660,47 +1666,118 @@ class MemoryEngine {
|
|
|
1660
1666
|
for (let i = ai + 1; i < Math.min(lines.length, ai + 6); i++) {
|
|
1661
1667
|
const m = lines[i].match(/^ {2}(provider|model): *(.+?) *$/)
|
|
1662
1668
|
if (!m) break
|
|
1663
|
-
if (m[1] === 'provider')
|
|
1664
|
-
if (m[1] === 'model') {
|
|
1669
|
+
if (m[1] === 'provider') defProvider = m[2].trim()
|
|
1670
|
+
if (m[1] === 'model') { defModel = m[2].trim(); break }
|
|
1665
1671
|
}
|
|
1666
1672
|
}
|
|
1667
1673
|
// 2.2.4 修复:settings.yaml 的 llm-deepseek 段是 flow 风格 YAML(`{ models: [ { id: x, contextWindow: 1000000, … } ] }`),
|
|
1668
1674
|
// 旧解析器只认 block 风格 → 整段解析失败、窗口退化为 fallback。改为纯函数解析(block + flow 双支持)。
|
|
1669
|
-
|
|
1675
|
+
const windows = parseModelWindowsPre(text)
|
|
1676
|
+
// 2026-09-08:优先按「会话自己的模型」查窗口——用户在 GUI 切过模型后,agent-default-model 与新会话默认值
|
|
1677
|
+
// 并不等于当前会话实际模型(实测会话跑 deepseek-official/deepseek-v4.1-flash,默认模型却是 opencode-go/deepseek-v4-flash)。
|
|
1678
|
+
const lookupProvider = provider || defProvider
|
|
1679
|
+
const lookupModel = model || defModel
|
|
1680
|
+
if (lookupModel) value = pickWindowPre(windows, lookupProvider, lookupModel)
|
|
1681
|
+
if (value > 0) { provider = lookupProvider; model = lookupModel }
|
|
1682
|
+
else if (defModel && defModel !== lookupModel) {
|
|
1683
|
+
const alt = pickWindowPre(windows, defProvider, defModel)
|
|
1684
|
+
if (alt > 0) { value = alt; provider = defProvider; model = defModel }
|
|
1685
|
+
}
|
|
1670
1686
|
}
|
|
1671
1687
|
const result = value > 0
|
|
1672
1688
|
? { window: value, source: 'auto' + (provider ? ':' + provider : '') + (model ? '/' + model : ''), model: model || '' }
|
|
1673
|
-
: { window: 131072, source: 'fallback', model: model || '' }
|
|
1674
|
-
this._waterWindowCache = { at: now, value: result }
|
|
1689
|
+
: { window: 131072, source: 'fallback', model: model || defModel || '' }
|
|
1690
|
+
this._waterWindowCache = { at: now, key: cacheKey, value: result }
|
|
1675
1691
|
return result
|
|
1676
1692
|
} catch (e) { return { window: 131072, source: 'fallback', error: String(e && e.message) } }
|
|
1677
1693
|
}
|
|
1678
1694
|
|
|
1695
|
+
/** 会话级水位记录(2026-09-08):水位此前是**全局单值**,只在有 agent 轮次时更新;
|
|
1696
|
+
* 于是切到别的会话时,卡片显示的是「上一个会话的数」或重启后的 0,且模型名只能拿默认模型凑。
|
|
1697
|
+
* 现在:①checkWaterLevel 每次测量写入 per-session 表;②查询时按 sessionId 取;
|
|
1698
|
+
* ③没有记录就从该会话日志(磁盘)推导窗口与模型——切会话即可刷新,不需要先发消息。 */
|
|
1699
|
+
rememberWaterRecord(sid, rec) {
|
|
1700
|
+
if (!sid || !rec) return
|
|
1701
|
+
if (!this._waterBySession) this._waterBySession = new Map()
|
|
1702
|
+
this._waterBySession.set(this.waterKey(sid), rec)
|
|
1703
|
+
if (this._waterBySession.size > 40) {
|
|
1704
|
+
const first = this._waterBySession.keys().next()
|
|
1705
|
+
if (!first.done) this._waterBySession.delete(first.value)
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1709
|
+
/** 会话 id 归一化:agent.session.id 与 GUI 快照 current 可能带/不带 `session-` 前缀,统一去掉后作键。 */
|
|
1710
|
+
waterKey(sid) {
|
|
1711
|
+
return String(sid || '').replace(/^session-/, '')
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
/** 取该会话最近 10 分钟内的实测记录(无则 null)。 */
|
|
1715
|
+
waterRecordFor(sid) {
|
|
1716
|
+
if (!sid) return null
|
|
1717
|
+
const m = this._waterBySession
|
|
1718
|
+
const rec = m && typeof m.get === 'function' ? m.get(this.waterKey(sid)) : null
|
|
1719
|
+
return rec && Date.now() - (Number(rec.at) || 0) < 600000 ? rec : null
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/** 从会话日志(磁盘)推导窗口与模型:不需要 agent,切换会话即可得到正确的模型名与窗口。 */
|
|
1723
|
+
async waterWindowForSession(sid) {
|
|
1724
|
+
const empty = { window: 0, source: '', model: '', provider: '', contextWindow: 0 }
|
|
1725
|
+
if (!sid) return empty
|
|
1726
|
+
if (!this._waterDeriveCache) this._waterDeriveCache = new Map()
|
|
1727
|
+
const cached = this._waterDeriveCache.get(String(sid))
|
|
1728
|
+
if (cached && Date.now() - cached.at < 300000) return cached.value
|
|
1729
|
+
let value = empty
|
|
1730
|
+
try {
|
|
1731
|
+
const dir = await this.locateSessionDir(String(sid))
|
|
1732
|
+
if (dir) {
|
|
1733
|
+
const raw = await readFile(path.join(dir, 'session.jsonl.zstd')).catch(() => null)
|
|
1734
|
+
const events = raw ? decodeZstdFrames(raw).split('\n').map((l) => { try { return JSON.parse(l) } catch (e) { return null } }).filter(Boolean) : []
|
|
1735
|
+
const info = findSessionModelPre(events)
|
|
1736
|
+
const wi = await this.resolveWaterWindow(info.provider, info.model)
|
|
1737
|
+
value = {
|
|
1738
|
+
window: info.contextWindow > 0 ? info.contextWindow : (Number(wi.window) || 0),
|
|
1739
|
+
source: info.contextWindow > 0 ? 'official-context' : (wi.source || ''),
|
|
1740
|
+
model: info.model || wi.model || '',
|
|
1741
|
+
provider: info.provider,
|
|
1742
|
+
contextWindow: info.contextWindow,
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
} catch (e) {}
|
|
1746
|
+
this._waterDeriveCache.set(String(sid), { at: Date.now(), value })
|
|
1747
|
+
if (this._waterDeriveCache.size > 40) {
|
|
1748
|
+
const first = this._waterDeriveCache.keys().next()
|
|
1749
|
+
if (!first.done) this._waterDeriveCache.delete(first.value)
|
|
1750
|
+
}
|
|
1751
|
+
return value
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1679
1754
|
/** M-CM6-A·水位 v2:计量优先级=ctx.tokenMeter.measure(官方计量,与聊天框 context ring 同源,免疫重启/压缩/chunk 洪流)→ 启发式估算(降级);
|
|
1680
1755
|
* 窗口优先级=waterLevelWindowTokens 手动覆盖 > 官方 request/context 容量 > settings.yaml auto/fallback。越阈值或检测到 compaction 事件→advisory+自动写抽取性骨架账本。 */
|
|
1681
1756
|
async checkWaterLevel(agent) {
|
|
1682
1757
|
try {
|
|
1683
1758
|
if (this.config.handoffEnabled === false) return
|
|
1684
|
-
|
|
1759
|
+
// 会话真实模型 + 官方容量:同一份事件扫描结果按会话缓存 5 分钟
|
|
1760
|
+
// (该事件只在请求头变化时追加,实测 2999 条事件里仅 2 条且位于会话开头,旧实现只扫最近 256 条 → 长会话永远扫不到)。
|
|
1761
|
+
let sessModel = { provider: '', model: '', contextWindow: 0 }
|
|
1762
|
+
try {
|
|
1763
|
+
const sid = agent && agent.session && agent.session.id ? String(agent.session.id) : ''
|
|
1764
|
+
const cached = this._officialWindowCache
|
|
1765
|
+
if (cached && sid && cached.sid === sid && Date.now() - cached.at < 300000 && cached.info) {
|
|
1766
|
+
sessModel = cached.info
|
|
1767
|
+
} else {
|
|
1768
|
+
sessModel = findSessionModelPre(sessionEventsOf(agent && agent.session))
|
|
1769
|
+
this._officialWindowCache = { sid, at: Date.now(), info: sessModel }
|
|
1770
|
+
}
|
|
1771
|
+
} catch (eWin) {}
|
|
1772
|
+
// 窗口解析按「会话真实模型」查表(2026-09-08):此前恒用 settings.yaml 的 agent-default-model,
|
|
1773
|
+
// 用户切过模型后卡片会标出另一个模型的名字(实测会话跑 deepseek-official/deepseek-v4.1-flash,却标 opencode-go/deepseek-v4-flash)。
|
|
1774
|
+
const winInfo = await this.resolveWaterWindow(sessModel.provider, sessModel.model)
|
|
1685
1775
|
let win = Number(winInfo && winInfo.window) || 0
|
|
1686
1776
|
let winSource = (winInfo && winInfo.source) || ''
|
|
1687
|
-
if (win > 0 && !/^manual/.test(winSource)) {
|
|
1688
|
-
// 官方容量:request/context
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
// 现在全量向前扫 + 按会话 id 缓存 5 分钟,避免每轮重复遍历。
|
|
1692
|
-
try {
|
|
1693
|
-
const sid = agent && agent.session && agent.session.id ? String(agent.session.id) : ''
|
|
1694
|
-
const cached = this._officialWindowCache
|
|
1695
|
-
let cw = 0
|
|
1696
|
-
if (cached && sid && cached.sid === sid && Date.now() - cached.at < 300000) {
|
|
1697
|
-
cw = cached.window
|
|
1698
|
-
} else {
|
|
1699
|
-
cw = findOfficialContextWindowPre(sessionEventsOf(agent && agent.session))
|
|
1700
|
-
this._officialWindowCache = { sid, at: Date.now(), window: cw }
|
|
1701
|
-
}
|
|
1702
|
-
if (cw > 0) { win = cw; winSource = 'official-context' }
|
|
1703
|
-
} catch (eWin) {}
|
|
1777
|
+
if (win > 0 && !/^manual/.test(winSource) && sessModel.contextWindow > 0) {
|
|
1778
|
+
// 官方容量:request/context 的 contextWindow(路由权威值,last-wins)
|
|
1779
|
+
win = sessModel.contextWindow
|
|
1780
|
+
winSource = 'official-context'
|
|
1704
1781
|
}
|
|
1705
1782
|
if (win <= 0) return
|
|
1706
1783
|
const rt = this.runtimeFor(agent)
|
|
@@ -1733,7 +1810,9 @@ class MemoryEngine {
|
|
|
1733
1810
|
}
|
|
1734
1811
|
}
|
|
1735
1812
|
} catch (e) {}
|
|
1736
|
-
|
|
1813
|
+
// 旧实现硬截断到 1.5 → 任何超额水位都显示成 150%,掩盖真实占用(用户实测 761692/131072 被显示为 150%)。
|
|
1814
|
+
// 现在如实上报,仅做防溢出的 99 倍上限。
|
|
1815
|
+
const ratio = win > 0 && Number.isFinite(estTokens) ? Math.min(estTokens / win, 99) : 0
|
|
1737
1816
|
rt.waterLevel = ratio
|
|
1738
1817
|
rt.waterLevelTokens = estTokens
|
|
1739
1818
|
this.state.waterLevelRatio = ratio
|
|
@@ -1741,9 +1820,14 @@ class MemoryEngine {
|
|
|
1741
1820
|
this.state.waterLevelWindow = win
|
|
1742
1821
|
this.state.waterLevelSource = winSource
|
|
1743
1822
|
this.state.waterLevelMeter = meterLayer
|
|
1744
|
-
this.state.waterLevelModel = winInfo.model || ''
|
|
1823
|
+
this.state.waterLevelModel = sessModel.model || winInfo.model || ''
|
|
1745
1824
|
this.state.waterLevelAt = Date.now()
|
|
1746
|
-
const threshold = Math.max(Number(this.config.waterLevelThreshold) || 0.
|
|
1825
|
+
const threshold = Math.max(Number(this.config.waterLevelThreshold) || 0.75, 0.1)
|
|
1826
|
+
// 会话级记录:切会话时 /handoff-state?sessionId= 直接取这份,不再显示别的会话的数
|
|
1827
|
+
this.rememberWaterRecord(agent && agent.session && agent.session.id ? String(agent.session.id) : '', {
|
|
1828
|
+
ratio, tokens: estTokens, window: win, source: winSource, meter: meterLayer,
|
|
1829
|
+
model: this.state.waterLevelModel || '', threshold, at: Date.now(), live: true,
|
|
1830
|
+
})
|
|
1747
1831
|
const over = ratio >= threshold || (ratio >= 0.5 && compacted)
|
|
1748
1832
|
if (!over) return
|
|
1749
1833
|
if (!rt.waterLevelAdvised || compacted) {
|
|
@@ -1968,7 +2052,7 @@ class MemoryEngine {
|
|
|
1968
2052
|
}
|
|
1969
2053
|
|
|
1970
2054
|
/** M-CM1 白板面板数据:PLAN 当前版+归档版本列表+账本列表;fileQ 非空时返回指定文件文本(严格白名单,限 handoff 目录内)。 */
|
|
1971
|
-
async handoffPanelData(fileQ) {
|
|
2055
|
+
async handoffPanelData(fileQ, sessionId) {
|
|
1972
2056
|
if (this.config.handoffEnabled === false) return { enabled: false }
|
|
1973
2057
|
const handoffDir = this.state.handoffDir || (await this.resolvePaths(undefined)).handoffDir
|
|
1974
2058
|
const planPath = this.state.planPath || path.join(handoffDir, 'PLAN.md')
|
|
@@ -1994,13 +2078,43 @@ class MemoryEngine {
|
|
|
1994
2078
|
if (st && st.isFile()) ledgers.push({ name: n, mtime: Number(st.mtimeMs) })
|
|
1995
2079
|
}
|
|
1996
2080
|
ledgers.sort((a, b) => b.mtime - a.mtime)
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2081
|
+
// 水位:优先返回「该会话」的记录(实测 → 磁盘推导);未指定 sessionId 时退回全局单值(兼容旧调用方)
|
|
2082
|
+
let water = null
|
|
2083
|
+
if (sessionId) {
|
|
2084
|
+
const rec = this.waterRecordFor(sessionId)
|
|
2085
|
+
if (rec) {
|
|
2086
|
+
water = {
|
|
2087
|
+
ratio: Number(rec.ratio) || 0, tokens: Number(rec.tokens) || 0, window: Number(rec.window) || 0,
|
|
2088
|
+
source: rec.source || '', meter: rec.meter || '', model: rec.model || '',
|
|
2089
|
+
threshold: Number(this.config.waterLevelThreshold) || 0.75, at: Number(rec.at) || 0, live: true,
|
|
2090
|
+
}
|
|
2091
|
+
} else {
|
|
2092
|
+
const wi = await this.waterWindowForSession(sessionId)
|
|
2093
|
+
water = {
|
|
2094
|
+
ratio: 0, tokens: 0, window: Number(wi.window) || 0, source: wi.source || '', meter: '',
|
|
2095
|
+
model: wi.model || '', threshold: Number(this.config.waterLevelThreshold) || 0.75, at: 0, live: false,
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
} else {
|
|
2099
|
+
if (!this.state.waterLevelWindow && this.config.handoffEnabled !== false) {
|
|
2100
|
+
try {
|
|
2101
|
+
const wi = await this.resolveWaterWindow()
|
|
2102
|
+
this.state.waterLevelWindow = Number(wi && wi.window) || 0
|
|
2103
|
+
this.state.waterLevelSource = (wi && wi.source) || ''
|
|
2104
|
+
this.state.waterLevelModel = (wi && wi.model) || ''
|
|
2105
|
+
} catch (e) {}
|
|
2106
|
+
}
|
|
2107
|
+
water = {
|
|
2108
|
+
ratio: Number(this.state.waterLevelRatio) || 0,
|
|
2109
|
+
tokens: Number(this.state.waterLevelTokens) || 0,
|
|
2110
|
+
window: Number(this.state.waterLevelWindow) || 0,
|
|
2111
|
+
source: this.state.waterLevelSource || '',
|
|
2112
|
+
meter: this.state.waterLevelMeter || '',
|
|
2113
|
+
model: this.state.waterLevelModel || '',
|
|
2114
|
+
threshold: Number(this.config.waterLevelThreshold) || 0.75,
|
|
2115
|
+
at: Number(this.state.waterLevelAt) || 0,
|
|
2116
|
+
live: Number(this.state.waterLevelAt) > 0,
|
|
2117
|
+
}
|
|
2004
2118
|
}
|
|
2005
2119
|
const planMt = await stat(planPath).then((s) => Number(s.mtimeMs)).catch(() => 0)
|
|
2006
2120
|
const sid = this.currentSessionId()
|
|
@@ -2014,16 +2128,7 @@ class MemoryEngine {
|
|
|
2014
2128
|
refresh: this.config.autoContinueRefreshRitual === false || !sid
|
|
2015
2129
|
? { sessionId: '', prompt: '' }
|
|
2016
2130
|
: { sessionId: sid, prompt: this.refreshRitualPrompt() },
|
|
2017
|
-
waterLevel:
|
|
2018
|
-
ratio: Number(this.state.waterLevelRatio) || 0,
|
|
2019
|
-
tokens: Number(this.state.waterLevelTokens) || 0,
|
|
2020
|
-
window: Number(this.state.waterLevelWindow) || 0,
|
|
2021
|
-
source: this.state.waterLevelSource || '',
|
|
2022
|
-
meter: this.state.waterLevelMeter || '',
|
|
2023
|
-
model: this.state.waterLevelModel || '',
|
|
2024
|
-
threshold: Number(this.config.waterLevelThreshold) || 0.8,
|
|
2025
|
-
at: Number(this.state.waterLevelAt) || 0,
|
|
2026
|
-
},
|
|
2131
|
+
waterLevel: water,
|
|
2027
2132
|
}
|
|
2028
2133
|
}
|
|
2029
2134
|
|
|
@@ -2782,7 +2887,7 @@ class MemoryEngine {
|
|
|
2782
2887
|
lines.push('\n[交接续命] 当前会话未绑定工作区(项目记忆不可用)。全局最近交接账本: ' + s.globalLedgerPath + ' —— 需要续接上次任务时,直接读取该文件恢复上下文;工作区绑定后白板/账本即恢复正常注入。')
|
|
2783
2888
|
}
|
|
2784
2889
|
// M-CM4/M-CM5 水位建议:越阈注入交接+派子代理建议(advisory;10 分钟新鲜度;无人值守静默)
|
|
2785
|
-
if (cfg.waterLevelAdvisory !== false && !this.isUnattendedNow() && (s.waterLevelRatio || 0) >= (Number(cfg.waterLevelThreshold) || 0.
|
|
2890
|
+
if (cfg.waterLevelAdvisory !== false && !this.isUnattendedNow() && (s.waterLevelRatio || 0) >= (Number(cfg.waterLevelThreshold) || 0.75) && Date.now() - (s.waterLevelAt || 0) < 600000) {
|
|
2786
2891
|
lines.push('\n' + L('snapshotWaterTitle') + '\n' + L('snapshotWaterBody', { pct: Math.round((s.waterLevelRatio || 0) * 100) }))
|
|
2787
2892
|
}
|
|
2788
2893
|
let used = 0
|
|
@@ -6243,7 +6348,7 @@ export function apply(ctx, config) {
|
|
|
6243
6348
|
if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
|
|
6244
6349
|
try {
|
|
6245
6350
|
const url = new URL(req.url, 'http://127.0.0.1')
|
|
6246
|
-
return writeJson(res, 200, await engine.handoffPanelData(url.searchParams.get('file')))
|
|
6351
|
+
return writeJson(res, 200, await engine.handoffPanelData(url.searchParams.get('file'), url.searchParams.get('sessionId')))
|
|
6247
6352
|
} catch (e) { return writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
|
|
6248
6353
|
},
|
|
6249
6354
|
},
|
package/lib/water-window.js
CHANGED
|
@@ -67,6 +67,38 @@ export function pickWindowPre(windows, provider, model) {
|
|
|
67
67
|
return 0
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/**
|
|
71
|
+
* 从会话事件里取最近一条 request/header|request/context 的真实 provider/model(+ 顺带 contextWindow)。
|
|
72
|
+
*
|
|
73
|
+
* 背景(2026-09-08 用户实测):水位卡此前把「窗口来源」标成 settings.yaml 的 agent-default-model(新会话默认模型),
|
|
74
|
+
* 用户在 GUI 里切过默认模型后,当前会话实际跑的模型与卡片显示不符
|
|
75
|
+
* (实测:会话日志 request/header = deepseek-official/deepseek-v4.1-flash-expires-on-0910,卡片却写 opencode-go/deepseek-v4-flash)。
|
|
76
|
+
* 会话自己的请求头才是「这个会话正在用哪个模型」的权威来源;默认模型只作兜底。
|
|
77
|
+
*
|
|
78
|
+
* @param {object[]} events - 会话事件数组。
|
|
79
|
+
* @param {number} maxScan - 最多向前扫多少条(默认全部)。
|
|
80
|
+
* @returns {{provider:string,model:string,contextWindow:number}} 未找到时字段为空/0。
|
|
81
|
+
*/
|
|
82
|
+
export function findSessionModelPre(events, maxScan = 0) {
|
|
83
|
+
const out = { provider: '', model: '', contextWindow: 0 }
|
|
84
|
+
if (!Array.isArray(events) || !events.length) return out
|
|
85
|
+
const floor = maxScan > 0 ? Math.max(0, events.length - maxScan) : 0
|
|
86
|
+
for (let i = events.length - 1; i >= floor; i--) {
|
|
87
|
+
const ev = events[i]
|
|
88
|
+
const t = ev && ev.type
|
|
89
|
+
if (t !== 'request/header' && t !== 'request/context') continue
|
|
90
|
+
const d = (ev && ev.data) || {}
|
|
91
|
+
const cfg = (d.header && d.header.config) || d.config || {}
|
|
92
|
+
const provider = String(cfg.provider || d.provider || '')
|
|
93
|
+
const model = String(cfg.model || d.model || '')
|
|
94
|
+
const cw = Number(d.contextWindow) || 0
|
|
95
|
+
if (!out.model && model) { out.model = model; out.provider = provider }
|
|
96
|
+
if (!out.contextWindow && cw > 0) out.contextWindow = cw
|
|
97
|
+
if (out.model && out.contextWindow) break
|
|
98
|
+
}
|
|
99
|
+
return out
|
|
100
|
+
}
|
|
101
|
+
|
|
70
102
|
/**
|
|
71
103
|
* 从会话事件里取最近一条 request/context 的 contextWindow(路由权威值)。
|
|
72
104
|
* 不限制扫描窗口:该事件只在请求头部变化时追加,长会话里往往只存在于开头。
|
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.5",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|