@a9i5k4/dsh-auto-memory 3.0.1 → 3.1.0

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 (124) hide show
  1. package/README.md +13 -8
  2. package/README.zh-CN.md +13 -8
  3. package/docs/HANDBOOK.md +88 -52
  4. package/docs/USER-GUIDE.en.md +9 -9
  5. package/docs/USER-GUIDE.zh-CN.md +9 -9
  6. package/docs/screenshots/promo/promo-0-banner-v4.png +0 -0
  7. package/docs/screenshots/promo/promo-1b-auto-recall.png +0 -0
  8. package/lib/activation-host.js +6 -1
  9. package/lib/client.js +819 -272
  10. package/lib/context-host.js +7 -1
  11. package/lib/episodic-store.js +90 -16
  12. package/lib/fact-store.js +463 -41
  13. package/lib/hub-io.js +217 -0
  14. package/lib/index.js +224 -45
  15. package/lib/intent-clean-safe.js +1 -1
  16. package/lib/memory-hub.js +37 -5
  17. package/lib/note-status.js +9 -1
  18. package/lib/procedure-store.js +252 -31
  19. package/lib/procedure-switch.js +38 -0
  20. package/lib/python-sidecar-client.js +285 -8
  21. package/package.json +6 -2
  22. package/docs/internal/ACCEPT-35-LIVE.md +0 -143
  23. package/docs/internal/ACCEPTANCE-20260914.md +0 -90
  24. package/docs/internal/ARCH-REVIEW-BRIEF.md +0 -411
  25. package/docs/internal/ARCH-REVIEW-REQUEST.md +0 -201
  26. package/docs/internal/ARCH-REVIEW-ROUND2.md +0 -169
  27. package/docs/internal/ARCH-REVIEW-ROUND3.md +0 -206
  28. package/docs/internal/ARCHITECTURE-FOR-ZCODE-20260920.md +0 -397
  29. package/docs/internal/ART-DIRECTION-DEEPSEEK-20260920.md +0 -351
  30. package/docs/internal/ART-DIRECTION-WIREFRAME.md +0 -191
  31. package/docs/internal/ART-DIRECTION-WIREFRAME.md.bak-superseded +0 -181
  32. package/docs/internal/AUDIT-WB-GRAPH-FULL-20260916.md +0 -314
  33. package/docs/internal/BATTLE-PLAN-20260917.md +0 -871
  34. package/docs/internal/CONCURRENCY-INVESTIGATION-20260917.md +0 -192
  35. package/docs/internal/CROSS-SESSION-SEARCH-PATH-DECISION.md +0 -72
  36. package/docs/internal/CROSS-SESSION-SEARCH-RESEARCH.md +0 -131
  37. package/docs/internal/CUA-VISION-FIX-NOTES.md +0 -78
  38. package/docs/internal/DECISIONS-20260914-SESSION.md +0 -269
  39. package/docs/internal/DESIGN-OVERHAUL-PRE-RESEARCH.md +0 -292
  40. package/docs/internal/DESIGN-P1-STATE-COMMIT-20260915.md +0 -219
  41. package/docs/internal/DIRECTION-CHECK-WB-GRAPH-20260916.md +0 -132
  42. package/docs/internal/FEATURE-INVENTORY.md +0 -531
  43. package/docs/internal/FEEDBACK-TO-DSHAPI-RELAY.md +0 -13
  44. package/docs/internal/G-SERIES-EXECUTION-20260917.md +0 -248
  45. package/docs/internal/G3-DESIGN-20260918.md +0 -82
  46. package/docs/internal/G3-DISK-FORMAT-GAP-20260919.md +0 -92
  47. package/docs/internal/GH-DISCUSSION-5732-COMMENT.md +0 -74
  48. package/docs/internal/GPT-ACCEPTANCE-PROMPT-20260916.md +0 -352
  49. package/docs/internal/GPT-REVIEW-PROMPT.md +0 -216
  50. package/docs/internal/GROUP-DIGEST-SETUP.md +0 -62
  51. package/docs/internal/GROUP-LISTENER-SETUP.md +0 -49
  52. package/docs/internal/GROUP-WEBHOOK-SETUP.md +0 -93
  53. package/docs/internal/HANDOFF-TO-ZCODE-20260920.md +0 -309
  54. package/docs/internal/HANDOFF-TO-ZCODE.md +0 -168
  55. package/docs/internal/HERMES-DATA-VERIFICATION-20260919.md +0 -120
  56. package/docs/internal/HERMES-LEGACY-STATUS-20260919.md +0 -74
  57. package/docs/internal/ISSUE-55-58-VERIFICATION-20260918.md +0 -175
  58. package/docs/internal/ISSUE10-FIX-EXECUTION-20260919.md +0 -389
  59. package/docs/internal/ISSUE10-PLAN-20260919.md +0 -254
  60. package/docs/internal/ISSUE10B-FORENSICS-20260919.md +0 -468
  61. package/docs/internal/ISSUE9-PURGE-AND-R1-PLAIN-20260919.md +0 -150
  62. package/docs/internal/ISSUE9-RESIDUAL-FORENSICS-20260919.md +0 -114
  63. package/docs/internal/KICKOFF-P0.md +0 -254
  64. package/docs/internal/LESSON-TO-CANDIDATE-STATUS-20260919.md +0 -79
  65. package/docs/internal/MASTER-PLAN-3.0.md +0 -411
  66. package/docs/internal/MEMORY-GOVERNANCE-20260917.md +0 -309
  67. package/docs/internal/MEMORY-MUTATION-AND-INDEX-DESIGN.md +0 -85
  68. package/docs/internal/MERGE-CONFLICT-SCAN-20260914.md +0 -222
  69. package/docs/internal/NEXT-VERSION-TODO.md +0 -95
  70. package/docs/internal/OFFICIAL-DISCUSSION-DRAFT.md +0 -80
  71. package/docs/internal/PENDING-FIXES-20260916.md +0 -289
  72. package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md +0 -705
  73. package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md.bak-s10 +0 -649
  74. package/docs/internal/PROCEDURAL-MEMORY-AND-APPROVAL-DESIGN-20260918.md +0 -225
  75. package/docs/internal/PROGRESS-20260917.md +0 -93
  76. package/docs/internal/PROMPT-GAP-AUDIT-20260920.md +0 -128
  77. package/docs/internal/R1-DEGRADE-AUDIT-20260918.md +0 -163
  78. package/docs/internal/R1-READABILITY-FORENSICS-20260919.md +0 -127
  79. package/docs/internal/R2-EVIDENCE-DEEP-AUDIT-20260918.md +0 -140
  80. package/docs/internal/R3-DEGRADE-LEDGER-DESIGN-20260918.md +0 -138
  81. package/docs/internal/R4-RECALL-QUOTA-PLAN-20260918.md +0 -218
  82. package/docs/internal/RAG-KARPATHY-PROGRAM.md +0 -229
  83. package/docs/internal/RELEASE-PROCESS.md +0 -99
  84. package/docs/internal/REPORT-P0-NIGHTLY.md +0 -212
  85. package/docs/internal/REPORT-P5-ACCEPTANCE.md +0 -31
  86. package/docs/internal/REPORT-WB-GRAPH-NIGHTLY.md +0 -153
  87. package/docs/internal/RESUME-20260918.md +0 -171
  88. package/docs/internal/RESUME-20260919.md +0 -104
  89. package/docs/internal/REVIEW-WB-GRAPH-SELF.md +0 -81
  90. package/docs/internal/RHINELAB-TO-DEEPSEEK-FEASIBILITY.md +0 -198
  91. package/docs/internal/ROADMAP-20260917-WEEK.md +0 -439
  92. package/docs/internal/ROADMAP.md +0 -106
  93. package/docs/internal/RUN-P0-NIGHTLY.md +0 -227
  94. package/docs/internal/S10-CONSTRUCTION-HANDOFF-20260917.md +0 -185
  95. package/docs/internal/S10-GAP-INVENTORY-20260917.md +0 -239
  96. package/docs/internal/S10-GAPS-PLAIN-20260917.md +0 -125
  97. package/docs/internal/SEMANTIC-ARCHITECTURE-SPEC.md +0 -360
  98. package/docs/internal/SESSION-FILE-REPAIR-PROTOCOL.md +0 -90
  99. package/docs/internal/SUBAGENT-REPORT-ROUTING-PRE-RESEARCH.md +0 -261
  100. package/docs/internal/T6-EXECUTION-20260920.md +0 -130
  101. package/docs/internal/TELEMETRY-EFFECT-REPORT-DESIGN-20260918.md +0 -146
  102. package/docs/internal/THESIS-GAP-ANALYSIS-20260918.md +0 -89
  103. package/docs/internal/THESIS-OUTLINE-20260918.md +0 -147
  104. package/docs/internal/THREE-LAYER-CONTRACT.md +0 -219
  105. package/docs/internal/TODO-BACKLOG.md +0 -263
  106. package/docs/internal/TODO-GRAPH.html +0 -715
  107. package/docs/internal/TODO-GRAPH.html.bak-20260914-v2 +0 -493
  108. package/docs/internal/TODO-GRAPH.html.bak-20260915-alsfix +0 -710
  109. package/docs/internal/TODO-GRAPH.html.bak-20260915-p1 +0 -710
  110. package/docs/internal/TODO-GRAPH.html.bak-20260915-p6a-rev +0 -703
  111. package/docs/internal/TODO-GRAPH.html.bak-20260915-wshint +0 -710
  112. package/docs/internal/TODO-GRAPH.html.bak-20260916-batch +0 -715
  113. package/docs/internal/UPSTREAM-ISSUE-PR-TRIAGE-20260919.md +0 -297
  114. package/docs/internal/UPSTREAM-ISSUES-3RD-AUDIT-20260920.md +0 -104
  115. package/docs/internal/WB-FORMAT-CONVENTION.md +0 -112
  116. package/docs/internal/WB-GRAPH-DECISIONS-20260914.md +0 -71
  117. package/docs/internal/WB-GRAPH-INTEGRATION-PLAN.md +0 -386
  118. package/docs/internal/WB-GRAPH-RESEARCH-BRIEF.md +0 -118
  119. package/docs/internal/WB-GRAPH-RESEARCH-EXTERNAL.md +0 -228
  120. package/docs/internal/WB-GRAPH-RESEARCH-LOCAL.md +0 -190
  121. package/docs/internal/reviews/CLAIM-VERIFICATION-20260914.md +0 -56
  122. package/docs/internal/reviews/PLAN-gpt6astra-round2-20260914.md +0 -787
  123. package/docs/internal/reviews/REVIEW-gpt6astra-20260914.md +0 -112
  124. package/docs/internal/reviews/ROUND3-REVIEW-INTEGRATION-20260914.md +0 -230
package/lib/client.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * 3. settings.section — 自动记忆设置页(存储位置、注入、反思风格)
8
8
  * Data flows over /api/dsh-auto-memory/* (loopback-only host routes).
9
9
  */
10
- console.log('[dsh-auto-memory] client v3.0.1 fingerprint: index-self-heal')
10
+ console.log('[dsh-auto-memory] client v3.1.0 fingerprint: promotion-override')
11
11
  window.__ModuleLoader__.load({
12
12
  id: '@a9i5k4/dsh-auto-memory',
13
13
  factory: (require) => {
@@ -43,6 +43,21 @@ window.__ModuleLoader__.load({
43
43
  try { pinned = localStorage.getItem(PIN_KEY) === '1' } catch (ePin) {}
44
44
  // 几何状态:left/top/width/height,持久化到 localStorage(用户可拖动/缩放)
45
45
  var GEOM_KEY = 'dsh-auto-memory.panel.geom'
46
+ // 承载面(2026-09-21 用户拍板 · 二次修正):'bottom-left'(默认,左下角浮层) | 'page'(会话页) | 'both'(两者共存)。
47
+ // ★修正原因(用户实测反馈原话):「它现在是浮在整个页面上方的,而不是和对话轨迹、上下文、白板看板在一起,作为一个单独的一页」。
48
+ // 根因:首版把「顶部」做成 shell.overlay 覆盖条 —— 宿主只给插件 4 个槽位(sidebar/main/rightbar/shell.overlay),
49
+ // 浮层永远只能"盖"在界面上,做不出"并列的一页";要成为并列页必须走 conversation.view
50
+ // (官方「对话轨迹」dsh-client-ui-trajectory 与本插件「白板看板」都用它注册)。
51
+ // 两种承载面共享同一份内容/页签/数据(见下面的 panelTab 共享状态);本机偏好,仅 localStorage。
52
+ var POS_KEY = 'dsh-auto-memory.panel.pos'
53
+ var PANEL_POS_VALUES = { 'bottom-left': 1, page: 1, both: 1 }
54
+ var panelPos = 'both'
55
+ try {
56
+ var savedPos = localStorage.getItem(POS_KEY)
57
+ // 兼容上一版:旧值 'top'(覆盖条形态)已废弃 ⇒ 自动迁移为 'page'(会话页形态),用户不必手动重选
58
+ if (savedPos === 'top') savedPos = 'page'
59
+ if (PANEL_POS_VALUES[savedPos]) panelPos = savedPos
60
+ } catch (ePos) {}
46
61
  var DEFAULT_W = 440
47
62
  var DEFAULT_H = 560
48
63
  var DEFAULT_GAP = 16
@@ -117,6 +132,16 @@ window.__ModuleLoader__.load({
117
132
  return null
118
133
  }
119
134
  function emit() { listeners.forEach(function (fn) { try { fn() } catch (e) {} }) }
135
+ // 共享页签状态(2026-09-21):浮层与「记忆」会话页读同一个模块级变量 ⇒ 两处切页互相同步
136
+ // (用户诉求原话:「两者共存…要保证同步」)。不用两份 useState,从根上杜绝状态漂移。
137
+ // 两个承载面都订阅 emit()(走 useTick),所以 setPanelTab 只要 emit 就能让两处同时重渲染。
138
+ var panelTab = 'overview'
139
+ function setPanelTab(v) { if (panelTab !== v) { panelTab = v; emit() } }
140
+ // 承载面切换钩子(2026-09-21):conversation.view 的注册/注销在 apply 作用域内(refreshSurfaces),
141
+ // 而 setPanelPos 在本模块作用域 ⇒ 用一个模块级钩子搭桥:切到「会话页/两者」时当场把页签加上,
142
+ // 切回「左下角浮层」时把它摘掉(不必等重启或重进会话)。
143
+ var surfacesRefreshHook = null
144
+ function setSurfacesRefreshHook(fn) { surfacesRefreshHook = fn }
120
145
  var controller = {
121
146
  isOpen: function () { return panelOpen },
122
147
  isClosing: function () { return panelClosing },
@@ -155,6 +180,22 @@ window.__ModuleLoader__.load({
155
180
  try { localStorage.setItem(PIN_KEY, pinned ? '1' : '0') } catch (e) {}
156
181
  emit()
157
182
  },
183
+ // 承载面(2026-09-21):左下角浮层 / 会话页 / 两者共存。设置页改动立即生效(即时回显)。
184
+ panelPos: function () { return panelPos },
185
+ setPanelPos: function (v) {
186
+ panelPos = PANEL_POS_VALUES[v] ? v : 'both'
187
+ try { localStorage.setItem(POS_KEY, panelPos) } catch (e) {}
188
+ // 承载面切换要即时增删「记忆」会话页签(见 setSurfacesRefreshHook 的说明)
189
+ try { if (surfacesRefreshHook) surfacesRefreshHook() } catch (eRf) {}
190
+ // 即时回显(2026-09-21 用户实测:「切到两者共存后上面什么都没出现」):
191
+ // 位置改在设置页里,而面板若关着,整个 MemoryPanel 渲染 null ⇒ 用户看不到任何变化。
192
+ // 故切档时面板若关着就自动打开,让新位置当场可见(切回旧档同样打开,保证每次切换都可感知)。
193
+ if (!panelOpen) controller.open()
194
+ else emit()
195
+ },
196
+ // 共享页签(浮层 ↔ 会话页)
197
+ panelTab: function () { return panelTab },
198
+ setPanelTab: setPanelTab,
158
199
  }
159
200
 
160
201
  // ───────────────────────── 国际化 ─────────────────────────
@@ -216,6 +257,10 @@ window.__ModuleLoader__.load({
216
257
  kvAvail: '可用', kvUnavail: '不可用', kvDup: '项目笔记重复标题', kvDupCount: ' 个',
217
258
  kvMem: '记忆文件', kvMemUser: '用户', kvMemNotes: '笔记', kvMemLog: '日志', kvMemMissing: '缺失',
218
259
  kvWs: '当前工作区', kvWsUnknown: '(未取到)', kvApi: 'API 连通', unknown: '(未知)',
260
+ // ★P0-2/P0-3(2026-09-22):把后端早已算好、前端零消费者的两组可观测性接到界面上。
261
+ dbgHubIo: '三层记忆落盘', dbgPrune: '事实淘汰(保留上限)',
262
+ kvPyTitle: 'Python 语义侧车', kvPyOff: '未启用(JS 端语义不受影响)', kvPyAlive: '运行中',
263
+ kvPyWatchdog: '看门狗击杀 / 重生', kvPyStderr: '最近 stderr 尾部', kvPyStderrTrunc: '(已截断)',
219
264
  updateTitle: 'dsh-auto-memory 已更新', updateSub: '本次更新内容:', gotIt: '知道了', noticeOpen: '了解更多',
220
265
  refresh: '刷新', refreshing: '刷新中…', close: '关闭', dragMove: '拖动移动', dragResize: '拖动调整大小', resetPos: '恢复默认位置', pinPanel: '钉住面板(点桌面其他地方不收起)', unpinPanel: '取消钉住(点面板外自动收起)',
221
266
  generated: '已生成', failed: '失败: ',
@@ -251,8 +296,13 @@ window.__ModuleLoader__.load({
251
296
  hubWhyCorrectionRate: function (r, cap) { return '它的纠正率是 ' + r + ',超过了上限 ' + cap + ' —— 说明这套流程本身有问题,需要先改掉。' },
252
297
  hubWhyHasCorrection: function (n) { return '它被纠正过 ' + n + ' 次 —— 有纠正记录说明这流程是错的,不能固化。' },
253
298
  hubWhyNoCriteria: '它没写「怎么算成功」—— 没有验收标准的流程不算技能。',
254
- hubWhyHighRisk: '它是高风险操作,需要你确认后才会固化。',
299
+ hubWhyHighRisk: '它是高风险操作,需要你人工确认才会固化 —— 点下面这个「批准(人工)」即可。',
255
300
  hubWhyObservationOnly: '它只是「看到过一件事」的线索,不是真正的流程 —— 不会自动晋升。',
301
+ // ★C(2026-09-22):三态说清 —— 「没有按钮」必须自己解释清楚,而不是让用户猜。
302
+ hubWhyStructural: '这道门是结构性的(内容本身不构成一条技能),人工也不能越过;补齐内容才会重新判定。',
303
+ hubWhyOverridable: '以上只是「证据还没攒够」的统计门,不是内容不合格 —— 你可以点右侧按钮人工越过。',
304
+ hubForcePromote: '强制晋升(人工)',
305
+ hubForcePromoteTitle: '人工越过跨会话数/成功次数两道统计门。结构门不会越过,且会在条目上留下「人工授权」痕迹。',
256
306
  hubEvSeen: '见过', hubEvSuccess: '成功', hubEvSessions: '跨会话', hubEvReuse: '被复用', hubEvCorrection: '被纠正',
257
307
  hubEvLine: function (ev) { return '见过 ' + (ev.seen || 0) + ' 次 · 成功 ' + (ev.success || 0) + ' 次 · 在 ' + (ev.sessions || 0) + ' 个会话里用过' + ((ev.correction || 0) ? ' · 被纠正过 ' + ev.correction + ' 次' : '') },
258
308
  hubInjectPreview: '晋升后会注入这些(点开看)',
@@ -293,6 +343,8 @@ window.__ModuleLoader__.load({
293
343
  saved: '已保存',
294
344
  settingsHeader: '记忆存储与行为设置(保存到 DSH 主目录 dsh-auto-memory.json):',
295
345
  fFontSize: '界面字号', fFontSizeHint: '记忆面板文字大小(立即生效,仅本机)', fsSm: '小', fsMd: '标准', fsLg: '大', fsXl: '特大',
346
+ fPanelPos: '记忆承载面', fPanelPosHint: '记忆内容的呈现方式:左下角浮层(可拖动缩放)/ 会话页(与「对话轨迹」「上下文」并列的一页,在会话页顶栏切换)/ 两者共存。两种承载面共享同一份内容与页签,实时同步;仅本机生效。',
347
+ posBottomLeft: '左下角(浮层)', posPage: '会话页(与对话轨迹并列)', posBoth: '两者共存',
296
348
  fUserDir: '用户记忆目录', fUserDirHint: '跨项目规则存放处,支持 ~ 开头;需有文件写权限。',
297
349
  fProjectDir: '项目记忆目录', fProjectDirHint: '相对各工作区的目录名(默认 .dsh-memory)。',
298
350
  fInject: '注入记忆上下文', fInjectHint: '每次组装提示词时自动注入 <memory_system> 块。',
@@ -325,6 +377,14 @@ window.__ModuleLoader__.load({
325
377
  fWaterAuto: '水位自动骨架账本', fWaterAutoHint: '越阈时自动写一篇系统骨架账本(每会话一次),防止模型忽视建议时交接材料缺失。',
326
378
  fWaterThreshold: '水位建议阈值', fWaterThresholdHint: '水位比例 = 上下文实占 ÷ 官方声明窗口(不再扣预留输出);超过该值(0.1-1.5)即触发交接建议/骨架账本;默认 0.75——官方自动压缩阈值是 80%,必须留出余量才来得及走完交接。',
327
379
  fHandoffPlan: '白板注入预算(字符)', fHandoffPlanHint: 'PLAN.md 全貌注入动态快照的硬截断预算;全文可经 memory_read 或面板白板页查看。默认 1200。',
380
+ fProcInject: '流程记忆注入(总闸)', fProcInjectHint: '★流程记忆(技能固化)的总闸:关闭后既不写入流程记忆,也不把它注入提示词。默认开。注意:设置页此前的旧键「技能固化与晋升」宿主已不再读取,要改请改这一项。',
381
+ fFactRetention: '事实条数上限', fFactRetentionHint: '事实层最多保留多少条,超出时自动淘汰:先淘汰已撤销的,再按写入时间从旧到新,重要项(置顶 / 用户级 / 显式写入 / 高置信度)最后淘汰。默认 1000。',
382
+ fTier0Catalog: '目录层(Tier-0 摘要)', fTier0CatalogHint: '开启时每轮注入一段「记忆目录」摘要(条目名 + 一句话结论),让模型先看到全局再按需下探;关闭则完全不注入目录层,省 token。默认开。',
383
+ fTier0Max: '目录层 token 上限', fTier0MaxHint: '目录层单次注入的 token 硬上限(与「注入预算 × 目录层占比」取小生效)。默认 400。需要装下更多语料时提高这里,而不是把注入预算整体推高。',
384
+ fRulesLayering: '规则段分层', fRulesLayeringHint: '开启后规则类记忆单独成段、每轮在场且不参与裁剪(旧行为是把规则混进记忆流一起裁)。关闭 = 完全回到旧行为。默认开。',
385
+ fCriteriaGate: '账本判据门', fCriteriaGateHint: '开启时白板/账本小节必须满足判据(H1-H4 / S1-S4)才被采纳,防止把思考过程当结论固化。关闭只退掉这道质量门,骨架 fail-soft 仍无条件生效。默认开。',
386
+ fWsDiscover: '工作区发现上限', fWsDiscoverHint: '扫描本机会话时最多识别多少个工作区(按最近会话时间降序取前 N 个)。默认 200;机器上历史工作区很多时可调小以缩短扫描时间。',
387
+ fMemFileIndex: '记忆文件索引快照', fMemFileIndexHint: '开启后额外维护一份只读的记忆文件索引快照(供外部查询),有少量 IO 开销;默认关,关时零额外开销。',
328
388
  fHandoffLedger: '账本注入预算(字符)', fHandoffLedgerHint: '最新一篇交接账本注入动态快照的硬截断预算。默认 800。',
329
389
  fConsSchedule: '定时做梦式固化', fConsScheduleHint: '每天到点自动读最近日志发散提炼长期要点,写入项目笔记/用户级记忆(等价 memory_consolidate)。',
330
390
  fConsScheduleTime: '固化触发时间', fConsScheduleTimeHint: 'HH:MM,插件日界内每天一次;默认 09:30。命中时刻需宿主在线。',
@@ -403,6 +463,9 @@ window.__ModuleLoader__.load({
403
463
  kvAvail: 'available', kvUnavail: 'unavailable', kvDup: 'Duplicate note headings', kvDupCount: '',
404
464
  kvMem: 'Memory files', kvMemUser: 'user', kvMemNotes: 'notes', kvMemLog: 'log', kvMemMissing: 'missing',
405
465
  kvWs: 'Current workspace', kvWsUnknown: '(unknown)', kvApi: 'API probes', unknown: '(unknown)',
466
+ dbgHubIo: 'Layer-3 memory persistence', dbgPrune: 'Fact eviction (retention cap)',
467
+ kvPyTitle: 'Python sidecar', kvPyOff: 'disabled (JS-side semantics unaffected)', kvPyAlive: 'running',
468
+ kvPyWatchdog: 'Watchdog kills / respawns', kvPyStderr: 'Last stderr tail', kvPyStderrTrunc: ' (truncated)',
406
469
  updateTitle: 'dsh-auto-memory Updated', updateSub: 'What\'s new in this update:', gotIt: 'Got it', noticeOpen: 'Learn more',
407
470
  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)',
408
471
  generated: 'Generated', failed: 'Failed: ',
@@ -436,8 +499,12 @@ window.__ModuleLoader__.load({
436
499
  hubWhyCorrectionRate: function (r, cap) { return 'Correction rate ' + r + ' exceeds cap ' + cap + '.' },
437
500
  hubWhyHasCorrection: function (n) { return 'Has ' + n + ' correction(s) — a corrected flow must not be crystallized.' },
438
501
  hubWhyNoCriteria: 'No success criteria — a flow without acceptance criteria is not a skill.',
439
- hubWhyHighRisk: 'High risk — needs your confirmation before crystallizing.',
502
+ hubWhyHighRisk: 'High risk — it needs your manual approval. Click "approve (manual)" below to crystallize it.',
440
503
  hubWhyObservationOnly: 'This is an observation, not a real flow — it never auto-promotes.',
504
+ hubWhyStructural: 'This gate is structural (the entry is not a skill yet) — a human cannot override it either. Fill in the content and it gets re-evaluated.',
505
+ hubWhyOverridable: 'The above is only a statistical gate (not enough evidence yet), not a content failure — you can override it with the button on the right.',
506
+ hubForcePromote: 'Force promote (manual)',
507
+ hubForcePromoteTitle: 'Manually bypass the cross-session / success-count statistical gates. Structural gates are NOT bypassed, and a "manual authorization" trace is left on the entry.',
441
508
  hubEvSeen: 'seen', hubEvSuccess: 'success', hubEvSessions: 'sessions', hubEvReuse: 'reused', hubEvCorrection: 'corrected',
442
509
  hubEvLine: function (ev) { return 'seen ' + (ev.seen || 0) + ' · success ' + (ev.success || 0) + ' · used in ' + (ev.sessions || 0) + ' session(s)' + ((ev.correction || 0) ? ' · corrected ' + ev.correction + 'x' : '') },
443
510
  hubInjectPreview: 'This is what gets injected once promoted',
@@ -478,6 +545,8 @@ window.__ModuleLoader__.load({
478
545
  saved: 'Saved',
479
546
  settingsHeader: 'Memory storage & behavior (saved to ~/.dsh/dsh-auto-memory.json):',
480
547
  fFontSize: 'Panel font size', fFontSizeHint: 'Memory panel text size (applies immediately, this device only)', fsSm: 'Small', fsMd: 'Normal', fsLg: 'Large', fsXl: 'Extra large',
548
+ fPanelPos: 'Memory surface', fPanelPosHint: 'How memory content is presented: bottom-left floating card (draggable/resizable), a session page (a tab alongside "Trajectory"/"Context" in the conversation header), or both. Both surfaces share the same content and tabs, live-synced; this device only.',
549
+ posBottomLeft: 'Bottom-left (floating)', posPage: 'Session page (alongside Trajectory)', posBoth: 'Both, side by side',
481
550
  fUserDir: 'User memory dir', fUserDirHint: 'Cross-project rules; supports ~ prefix; needs write permission.',
482
551
  fProjectDir: 'Project memory dir', fProjectDirHint: 'Directory name relative to each workspace (default .dsh-memory).',
483
552
  fInject: 'Inject memory context', fInjectHint: 'Auto-inject <memory_system> block into every prompt.',
@@ -510,6 +579,14 @@ window.__ModuleLoader__.load({
510
579
  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.',
511
580
  fWaterThreshold: 'Water-level threshold', fWaterThresholdHint: 'The ratio counts session tokens against the official declared window (the reserved output budget is no longer deducted); past this value (0.1-1.5) the handoff advisory / skeleton ledger fires; default 0.75 — the official auto-compaction threshold is 80%, so a 5% margin is needed to finish the handoff first.',
512
581
  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.',
582
+ fProcInject: 'Procedure memory injection (master switch)', fProcInjectHint: 'Master switch for procedure memory (skill crystallization): when off, none is written and none is injected. On by default. The older "Skill crystallization" key is no longer read by the host, so change this one instead.',
583
+ fFactRetention: 'Fact retention limit', fFactRetentionHint: 'Maximum facts kept; when exceeded, revoked facts are pruned first, then oldest-first by write order, and important ones (pinned / user-level / explicit / high-confidence) last. Default 1000.',
584
+ fTier0Catalog: 'Tier-0 catalog layer', fTier0CatalogHint: 'When on, a short memory catalog (entry titles plus a one-line takeaway) is injected every round so the model sees the whole picture before drilling down. Off injects no catalog layer at all, saving tokens. On by default.',
585
+ fTier0Max: 'Tier-0 token cap', fTier0MaxHint: 'Hard per-injection token cap for the catalog layer, taking the minimum with (injection budget x catalog share). Default 400. Raise this rather than the whole injection budget when more corpus must fit.',
586
+ fRulesLayering: 'Layered rules section', fRulesLayeringHint: 'When on, rule-type memories form their own section that is always present and never trimmed (the legacy behavior mixed rules into the trimmable memory stream). Off is exactly the legacy behavior. On by default.',
587
+ fCriteriaGate: 'Ledger criteria gate', fCriteriaGateHint: 'When on, whiteboard/ledger sections must satisfy the criteria (H1-H4 / S1-S4) before being accepted, which prevents storing reasoning as conclusions. Off drops only this quality gate; the skeleton fail-soft path always applies. On by default.',
588
+ fWsDiscover: 'Workspace discovery cap', fWsDiscoverHint: 'Maximum workspaces identified when scanning local sessions (newest session time first). Default 200; lower it to speed up scanning when many historical workspaces exist.',
589
+ fMemFileIndex: 'Memory file index snapshot', fMemFileIndexHint: 'When on, an extra read-only index snapshot of memory files is maintained for external queries, at a small IO cost. Off by default, and off means zero overhead.',
513
590
  fHandoffLedger: 'Ledger injection budget (chars)', fHandoffLedgerHint: 'Hard truncation budget for injecting the latest handoff ledger. Default 800.',
514
591
  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).',
515
592
  fConsScheduleTime: 'Consolidation time', fConsScheduleTimeHint: 'HH:MM, once per plugin day; default 09:30. The host must be online at that moment.',
@@ -659,6 +736,23 @@ window.__ModuleLoader__.load({
659
736
 
660
737
  // ───────────────────────── 更新弹窗 / 首次指导 ─────────────────────────
661
738
  var CHANGELOG = {
739
+ '3.1.0': { zh: [
740
+ '新增:记忆中枢「**人工越权晋升**」——给「证据还没攒够」被挡住的自沉淀条目一个真正可用的人工通道。两类门现在分得很清:**统计门**(跨会话数/成功次数没攒够)=内容没问题、只是证据不足 ⇒ 给按钮,你可以人工越过;**结构门**(已弃用/只是观察线索/有纠正记录/没写「怎么算成功」/高风险未批准)=内容本身还不构成技能 ⇒ 不给按钮,并直接写明为什么。所以不会再出现「点了没反应」的假按钮。越权过的条目会留下「人工授权」痕迹,可审计。',
741
+ '新增:设置页按用途重排为 9 个分区(语义记忆总开关 / 记忆窗口(注入什么) / 记忆容量与归档 / 自动沉淀成技能 / 长会话接续 / 自动化与免打扰 / 存储与外部记忆 / 外观与交互 / 关于与诊断)。**控件一字未改**,只是搬进对应分区并改了分组标题——此前「记忆中枢的判定参数」和「注入配置」混在一个分区里,找东西全靠翻。',
742
+ '修复:「手动晋升的通道被关掉了」的真相——判定函数对线上 30 条自沉淀条目**全部**返回「不晋升」(跨会话数不足 13 / 已弃用 8 / 只是观察线索 8 / 成功次数不足 1),而按钮此前只在「判定=可晋升」时出现,所以一条也点不亮。现已按上面的语义重做。',
743
+ '说明:更深一层的源头**本轮只缓解、未根治**——证据里的「跨会话数」恒为 0(唯一写入方依赖一个默认关闭的桥接开关),所以证据永远攒不够;人工通道解决的是「攒不够时能不能人工放行」,不是「证据永不累积」本身。',
744
+ '优化:宽屏下卡片流转为多列,记忆页改成左侧分区导航 + 右侧内容区(窄屏自动回到顶部横排);顶栏与浮层两种承载面**出厂即共存**——更新后直接在会话页顶栏就能看到记忆入口。',
745
+ '优化:日志列顺序调整——「硬性约束(每轮必注入)」与「用户笔记」提到最上面,每日日志折进可展开的折叠区。',
746
+ '说明:宿主侧(记忆中枢判定链)有改动,**建议重启 dsh web** 让其生效;界面半边刷新页面即可。',
747
+ ], en: [
748
+ 'New: manual promotion override in the memory hub — a genuinely working human channel for self-consolidated entries blocked by "not enough evidence yet". The two kinds of gates are now distinct: **statistical** (cross-session count / success count not yet reached) = the content is fine, only the evidence is thin ⇒ a button is shown and you may override; **structural** (deprecated / mere observation / has corrections / no success criteria / high-risk not approved) = the content is not a skill yet ⇒ no button, with a plain-language reason instead. So the UI no longer offers a fake button that does nothing. Overridden entries keep an auditable "manual authorization" trace.',
749
+ 'New: the settings page is regrouped into 9 purpose-named sections (memory engine / memory window / capacity & retention / skills & promotion / handoff & continuation / automation / storage / appearance / about). **No control was changed** — they were only moved into the matching section with clearer group titles; previously promotion parameters and injection settings shared one section, so finding anything meant scrolling.',
750
+ 'Fix: the real reason the manual promotion channel appeared "off" — the decision function returned "do not promote" for **all 30** self-consolidated entries (13 not enough cross-session usage / 8 deprecated / 8 observation-only / 1 not enough successes), and the button only appeared for "promotable" rows, so not one row could light up. Rebuilt around the semantics above.',
751
+ 'Note: the deeper cause is **mitigated, not cured** in this release — the cross-session count in the evidence record is permanently 0 (its only writer depends on a bridge switch that defaults to off), so evidence never accumulates. The manual channel answers "can a human override when evidence is thin", not "why evidence never accumulates".',
752
+ 'Improved: card flows are multi-column on wide screens, and the memory page uses a left section nav with content on the right (narrow screens fall back to a top strip). The titlebar and overlay surfaces now **coexist out of the box** — the memory entry is right there in the session titlebar after updating.',
753
+ 'Improved: log column order — hard constraints (injected every turn) and user notes moved to the top, with the daily log folded into an expandable section.',
754
+ 'Note: the host side (promotion decision chain) changed — **a dsh web restart is recommended**; the UI half only needs a page refresh.',
755
+ ] },
662
756
  '3.0.1': { zh: [
663
757
  '修复:宿主控制台被 `restoreLastAgent: candidate rejected` **无限刷屏**(每 15 秒一条,打开就停不下来),同时「最近会话」恢复失效——刷新仪式与自动接续的兜底路径都受影响。根因是归属判据过宽:代码把「有父会话」等同于「是子代理」并一律排除,但**你点过一次接续后产生的新会话其实也有父会话**,它是你的正常会话,却被当成子代理拒掉了;列表里只剩它一个时,每个心跳周期都白试一次。现在改为按 `origin`/`delegationDepth` 精确识别子代理,接续会话正常放行;另给诊断信息加了 5 分钟节流兜底。',
664
758
  '顺带修好两处同样的误判:接续会话里的对话此前**不会自动沉淀**、工作区状态也**不刷新**——都因为同一条过宽判据。',
@@ -1235,6 +1329,11 @@ window.__ModuleLoader__.load({
1235
1329
  // 视觉:液态玻璃(毛玻璃)—— backdrop-filter + DSH 主题令牌(--dsw-alias-*),
1236
1330
  // 跟随亮/暗主题与页面背景自适应;位置/尺寸由 JS 几何状态驱动(可拖动、可缩放)。
1237
1331
  var CSS = [
1332
+ // ★2026-09-22 动效刻度作用域(MOTION-SPEC-20260922 §0.2):刻度此前只定义在 [data-dam-page] 内,
1333
+ // 而浮层 [data-dam-panel] 不是它的后代 ⇒ 浮层那组取不到变量,只能写字面量、无法与页面统一。
1334
+ // 本条把同一套刻度同时挂到两个承载面;page 侧原有的那份定义原样保留(不删不搬,避免层叠行为变化)。
1335
+ '[data-dam-panel], [data-dam-page] { --dam-dur-quick: 150ms; --dam-dur-fast: 250ms; --dam-dur-slow: 400ms; --dam-dur-stagger: 40ms;',
1336
+ ' --dam-ease-out: cubic-bezier(.22, 1, .36, 1); --dam-ease-inout: cubic-bezier(.65, 0, .35, 1); --dam-ease-pop: cubic-bezier(.34, 1.36, .64, 1); }',
1238
1337
  '[data-dam-panel] { position: fixed; left: 16px; width: 440px; height: 560px; --dam-scale: 1;',
1239
1338
  ' max-width: calc(100vw - 32px); max-height: calc(100vh - 32px);',
1240
1339
  ' display: flex; flex-direction: column; overflow: hidden; z-index: 3000; pointer-events: auto;',
@@ -1244,10 +1343,10 @@ window.__ModuleLoader__.load({
1244
1343
  ' border: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.35)) 65%, transparent);',
1245
1344
  ' box-shadow: 0 24px 64px rgba(0,0,0,.22), 0 4px 16px rgba(0,0,0,.10), inset 0 1px 0 rgba(255,255,255,.22);',
1246
1345
  ' color: var(--dsw-alias-label-primary, #1f2328);',
1247
- ' animation: dam-in .2s cubic-bezier(.2,.9,.3,1.15) both; transform-origin: left bottom; }',
1346
+ ' animation: dam-in var(--dam-dur-fast) cubic-bezier(.2,.9,.3,1.15) both; transform-origin: left bottom; }',
1248
1347
  '@keyframes dam-in { from { opacity: 0; transform: scale(.92) translateY(10px); } to { opacity: 1; transform: none; } }',
1249
1348
  '@keyframes dam-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: scale(.95) translateY(6px); } }',
1250
- '[data-dam-panel][data-closing="true"] { animation: dam-out .16s ease-in both; }',
1349
+ '[data-dam-panel][data-closing="true"] { animation: dam-out var(--dam-dur-quick) var(--dam-ease-out) both; }',
1251
1350
  '[data-dam-panel][data-scale="sm"] { --dam-scale: .9; }',
1252
1351
  '[data-dam-panel][data-scale="lg"] { --dam-scale: 1.15; }',
1253
1352
  '[data-dam-panel][data-scale="xl"] { --dam-scale: 1.3; }',
@@ -1264,14 +1363,115 @@ window.__ModuleLoader__.load({
1264
1363
  ' opacity: .55; z-index: 2; border-radius: 0 0 16px 0;',
1265
1364
  ' background: linear-gradient(135deg, transparent 50%, color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 45%, transparent) 50%); }',
1266
1365
  '[data-dam-resize]:hover { opacity: 1; }',
1366
+ // ★2026-09-21 二次修正:原「顶部通栏停靠条」CSS 已随该形态废弃而删除 —— 它曾把面板做成 shell.overlay 里的
1367
+ // 覆盖条(用户实测:「浮在整个页面上方,而不是和对话轨迹…并列的一页」)。现在第二承载面是 conversation.view 会话页,
1368
+ // 窄容器/宽容器两套样式见下面 [data-dam-page] 一组。
1369
+ // ── 会话页大屏适配(2026-09-22「大屏批」) ───────────────────────────────
1370
+ // 设计刻度:间距 4/8/12/16/24/32、圆角 8/12/16、字号 = 用户档位 × 视口台阶。
1371
+ // 动效刻度(transitions-motion token 表):150 / 250 / 400ms + smooth-out 曲线、错开 40ms。
1372
+ // 作用域只有 [data-dam-page](会话页) —— 浮层 [data-dam-panel] 保持逐字节不变。
1373
+ '[data-dam-page] { display: flex; flex-direction: column; width: 100%; height: 100%; min-height: 0;',
1374
+ ' --dam-scale: var(--dam-user-scale, 1);',
1375
+ ' --dam-measure: 1440px;',
1376
+ ' --dam-pad-x: clamp(16px, 1.6vw + 6px, 44px);',
1377
+ ' --dam-pad-y: clamp(14px, .8vw + 6px, 26px);',
1378
+ ' --dam-gap: clamp(10px, .5vw + 6px, 18px);',
1379
+ ' --dam-dur-quick: 150ms; --dam-dur-fast: 250ms; --dam-dur-slow: 400ms;',
1380
+ ' --dam-ease-out: cubic-bezier(.22, 1, .36, 1);',
1381
+ ' --dam-ease-inout: cubic-bezier(.65, 0, .35, 1);',
1382
+ ' --dam-ease-pop: cubic-bezier(.34, 1.36, .64, 1);',
1383
+ ' color: var(--dsw-alias-label-primary, #1f2328); font: 13px/1.55 system-ui, "Segoe UI", sans-serif; }',
1384
+ '[data-dam-page-head] { display: flex; align-items: center; gap: 8px; padding: 10px 4px;',
1385
+ ' border-bottom: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.25)) 55%, transparent); }',
1386
+ '[data-dam-page-head] strong { font-size: calc(15px * var(--dam-scale)); }',
1387
+ '[data-dam-page-head] .dam-spacer { flex: 1; }',
1388
+ // ★2026-09-22 左侧侧栏(方向文档 PANEL-PAGE-MIGRATION-DIRECTION §2/§3):顶部横向滚动页签 → 左侧固定侧栏;
1389
+ // 内容区独立成列独立滚动。窄窗(<900px)自动降级为顶部横条(只换方向,不注销任何分区)。
1390
+ '[data-dam-page-main] { display: flex; flex: 1; min-height: 0; }',
1391
+ '[data-dam-page-nav] { flex: 0 0 auto; width: 170px; overflow: auto; padding: 10px 8px; display: flex; flex-direction: column; gap: 2px;',
1392
+ ' border-right: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.25)) 55%, transparent); }',
1393
+ '[data-dam-page-nav] button { flex: 0 0 auto; border: 0; background: transparent; color: inherit; text-align: left; cursor: pointer;',
1394
+ ' padding: 7px 10px; border-radius: 8px; font: inherit; font-size: calc(12.5px * var(--dam-scale)); opacity: .68; white-space: nowrap;',
1395
+ ' transition: background-color var(--dam-dur-quick) var(--dam-ease-out), color var(--dam-dur-quick) var(--dam-ease-out), opacity var(--dam-dur-quick) var(--dam-ease-out); }',
1396
+ '[data-dam-page-nav] button[data-active="true"] { opacity: 1; font-weight: 650; color: var(--dam-accent, var(--dsw-alias-brand-primary, #4f7cff));',
1397
+ ' background: color-mix(in srgb, var(--dam-accent, #4f7cff) 12%, transparent); }',
1398
+ '[data-dam-page-content] { flex: 1; min-width: 0; display: flex; flex-direction: column; min-height: 0; }',
1399
+ '[data-dam-page-nav], [data-dam-page-content], [data-dam-page-main] { flex-shrink: 1; }',
1400
+ '[data-dam-page] [data-dam-section] { min-width: 0; }',
1401
+ // 折叠标题(日志列表用):整行可点, 箭头用 transform 旋转(不换字符,避免宽度跳动)。
1402
+ '[data-dam-page] [data-dam-fold] { display: flex; align-items: center; gap: 8px; width: 100%; text-align: left; border: 0; cursor: pointer;',
1403
+ ' background: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 8%, transparent); color: inherit; font: inherit;',
1404
+ ' font-size: calc(12.5px * var(--dam-scale)); font-weight: 650; padding: 9px 12px; border-radius: 9px;',
1405
+ ' transition: background-color var(--dam-dur-quick) var(--dam-ease-out); }',
1406
+ '[data-dam-page] [data-dam-fold]:hover { background: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 14%, transparent); }',
1407
+ '[data-dam-page] [data-dam-fold] .dam-fold-caret { display: inline-block; transition: transform var(--dam-dur-fast) var(--dam-ease-inout); }',
1408
+ '[data-dam-page] [data-dam-fold][data-open="true"] .dam-fold-caret { transform: rotate(90deg); }',
1409
+ '[data-dam-page] [data-dam-fold] .dam-fold-count { margin-left: auto; opacity: .55; font-weight: 400; }',
1410
+ '@media (max-width: 900px) {',
1411
+ ' [data-dam-page-main] { flex-direction: column; }',
1412
+ ' [data-dam-page-nav] { width: auto; flex-direction: row; gap: 4px; overflow-x: auto; overflow-y: hidden; padding: 6px 8px;',
1413
+ ' border-right: 0; border-bottom: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.25)) 55%, transparent); } }',
1414
+ '[data-dam-page] [data-dam-body] { flex: 1; min-height: 0; overflow: auto;',
1415
+ ' padding: var(--dam-pad-y) var(--dam-pad-x) 28px;',
1416
+ ' padding-inline: max(var(--dam-pad-x), calc((100% - var(--dam-measure)) / 2));',
1417
+ ' scrollbar-width: thin;',
1418
+ ' scrollbar-color: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 34%, transparent) transparent; }',
1419
+ '[data-dam-page] [data-dam-body]::-webkit-scrollbar { width: 10px; height: 10px; }',
1420
+ '[data-dam-page] [data-dam-body]::-webkit-scrollbar-track { background: transparent; }',
1421
+ '[data-dam-page] [data-dam-body]::-webkit-scrollbar-thumb { border: 3px solid transparent; background-clip: content-box;',
1422
+ ' border-radius: 8px; background-color: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 28%, transparent); }',
1423
+ '[data-dam-page] [data-dam-body]::-webkit-scrollbar-thumb:hover {',
1424
+ ' background-color: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 48%, transparent); }',
1425
+ // 大屏台阶:字号随视口抬两档, 与用户档位相乘, 不覆盖用户选择。
1426
+ '@media (min-width: 1400px) { [data-dam-page] { --dam-scale: calc(var(--dam-user-scale, 1) * 1.06); } }',
1427
+ '@media (min-width: 1800px) { [data-dam-page] { --dam-scale: calc(var(--dam-user-scale, 1) * 1.12); } }',
1428
+ // 首帧/页签切换:主体淡入 + 6px 上浮(只动 opacity/transform, 不触发布局与重绘)。
1429
+ '[data-dam-page] [data-dam-body] > :not([data-dam-flow]) { animation: dam-page-in var(--dam-dur-fast) var(--dam-ease-out) both; }',
1430
+ '@keyframes dam-page-in { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }',
1431
+ // ★卡片流分列(大屏核心收益):只对打了 [data-dam-flow] 标记的页签生效;
1432
+ // 非卡片子元素一律整行占据 ⇒ 不打标记的页签 = 逐字节旧行为, 打了标记也只有卡片进列。
1433
+ '[data-dam-page] [data-dam-flow] { display: grid; gap: var(--dam-gap); align-items: start; align-content: start;',
1434
+ ' grid-template-columns: repeat(auto-fit, minmax(min(100%, 380px), 1fr)); }',
1435
+ '[data-dam-page] [data-dam-flow] > * { grid-column: 1 / -1; min-width: 0; animation: dam-card-in var(--dam-dur-fast) var(--dam-ease-out) both; }',
1436
+ // ★2026-09-22 动效统一(MOTION-SPEC-20260922 §1.2/§2.2/§2.3):
1437
+ // ① 时长 400ms → var(--dam-dur-fast) 250ms —— 用途是「页面前进」,400ms 是「面板打开」刻度,用在切页签会拖。
1438
+ // ② 补 both —— 原缺 animation-fill-mode,而下面 nth-child 带 40~200ms 延迟;fill-mode 默认 none 时
1439
+ // 延迟期内动画不生效 ⇒ 卡片按终态(opacity:1)渲染 ⇒ 第 2~6 张「先闪一下 → 跳空 → 再淡入」。
1440
+ // 同文件的 dam-rise 写了 both(:1466/:1472),属漏写不是设计。
1441
+ // ③ 错开值改走 --dam-dur-stagger(数值不变,只刻度化)。
1442
+ '[data-dam-page] [data-dam-flow] > [data-dam-card] { grid-column: auto; margin-bottom: 0;',
1443
+ ' animation: dam-card-in var(--dam-dur-fast) var(--dam-ease-out) both; }',
1444
+ '@keyframes dam-card-in { from { opacity: 0; transform: translateY(8px) scale(.988); } to { opacity: 1; transform: none; } }',
1445
+ '[data-dam-page] [data-dam-flow] > [data-dam-card]:nth-child(2) { animation-delay: calc(var(--dam-dur-stagger) * 1); }',
1446
+ '[data-dam-page] [data-dam-flow] > [data-dam-card]:nth-child(3) { animation-delay: calc(var(--dam-dur-stagger) * 2); }',
1447
+ '[data-dam-page] [data-dam-flow] > [data-dam-card]:nth-child(4) { animation-delay: calc(var(--dam-dur-stagger) * 3); }',
1448
+ '[data-dam-page] [data-dam-flow] > [data-dam-card]:nth-child(5) { animation-delay: calc(var(--dam-dur-stagger) * 4); }',
1449
+ '[data-dam-page] [data-dam-flow] > [data-dam-card]:nth-child(n+6) { animation-delay: calc(var(--dam-dur-stagger) * 5); }',
1450
+ // 交互反馈:按压即时(150ms 刻度);悬停只在精确指针设备上生效, 避免触屏误触发。
1451
+ '[data-dam-page] [data-dam-btn], [data-dam-page] [data-dam-tab] {',
1452
+ ' transition: background-color var(--dam-dur-quick) var(--dam-ease-out), color var(--dam-dur-quick) var(--dam-ease-out),',
1453
+ ' opacity var(--dam-dur-quick) var(--dam-ease-out), transform var(--dam-dur-quick) var(--dam-ease-out); }',
1454
+ '[data-dam-page] [data-dam-btn]:active, [data-dam-page] [data-dam-tab]:active { transform: scale(.97); }',
1455
+ '[data-dam-page] [data-dam-tab] { padding: 8px 11px; }',
1456
+ '[data-dam-page] :focus-visible { outline: 2px solid color-mix(in srgb, var(--dam-accent, #2456c4) 72%, transparent); outline-offset: 2px; }',
1457
+ '@media (hover: hover) and (pointer: fine) { [data-dam-page] [data-dam-tab]:hover { opacity: .92; } }',
1458
+ // 减动效闸门:新增动效逐条归零(与文件既有那段同源, 此处只覆盖本批新增的选择器)。
1459
+ '@media (prefers-reduced-motion: reduce) {',
1460
+ ' [data-dam-page] [data-dam-body] > *, [data-dam-page] [data-dam-flow] > [data-dam-card] { animation: none !important; }',
1461
+ ' [data-dam-page] [data-dam-btn], [data-dam-page] [data-dam-tab] { transition: none !important; }',
1462
+ ' [data-dam-page] [data-dam-btn]:active, [data-dam-page] [data-dam-tab]:active { transform: none; } }',
1267
1463
  '[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; }',
1268
1464
  '[data-dam-btn]:hover { opacity: 1; background: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 16%, transparent); }',
1269
1465
  '[data-dam-btn][data-pin="on"] { opacity: 1; background: color-mix(in srgb, var(--dam-accent, #4f7cff) 24%, transparent); }',
1270
1466
  '[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); }',
1271
1467
  '[data-dam-tabs] { flex: 1; min-width: 0; overflow: hidden; padding: 5px 4px; scrollbar-width: none; -ms-overflow-style: none; }',
1272
1468
  '[data-dam-tabs]::-webkit-scrollbar { display: none; }',
1469
+ // ★2026-09-22 取证记录(MOTION-SPEC-20260922 §3.3,本批**不改值**):下面这条 transition: transform 目前是
1470
+ // **死声明** —— 全文件只有 3 处提到 data-dam-tab-strip(CSS 定义 / 减动效闸门 / :4714 创建该 div),
1471
+ // 滚动由父容器原生 scrollTo/scrollBy 驱动(:4704-4705/:4710),没有任何代码写过它的 transform。
1472
+ // 保留原样:删它对本轮问题零贡献,却可能挡住「将来把滚动改回 transform 驱动」。要动它请单开一批。
1273
1473
  '[data-dam-tab-strip] { display: flex; width: max-content; transition: transform .42s cubic-bezier(.22,.8,.2,1); will-change: transform; }',
1274
- '[data-dam-tab] { flex: 0 0 auto; min-width: max-content; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border: none; background: transparent; cursor: pointer; color: inherit; opacity: .68; padding: 6px 9px; border-radius: 7px; font-size: calc(12.5px * var(--dam-scale)); transition: color .25s ease, background .25s ease, opacity .25s ease, transform .25s ease; }',
1474
+ '[data-dam-tab] { flex: 0 0 auto; min-width: max-content; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; border: none; background: transparent; cursor: pointer; color: inherit; opacity: .68; padding: 6px 9px; border-radius: 7px; font-size: calc(12.5px * var(--dam-scale)); transition: color var(--dam-dur-quick) var(--dam-ease-out), background var(--dam-dur-quick) var(--dam-ease-out), opacity var(--dam-dur-quick) var(--dam-ease-out), transform var(--dam-dur-quick) var(--dam-ease-out); }',
1275
1475
  '[data-dam-tabs-arrow] { flex: 0 0 26px; height: 30px; border: 0; background: transparent; color: inherit; cursor: pointer; opacity: .55; font-size: 18px; }',
1276
1476
  '[data-dam-tabs-arrow]:hover:not(:disabled) { opacity: 1; background: color-mix(in srgb, var(--dam-accent, #2456c4) 16%, transparent); transform: scale(1.12); }',
1277
1477
  '[data-dam-tabs-arrow]:active:not(:disabled) { transform: scale(.92); }',
@@ -1308,21 +1508,31 @@ window.__ModuleLoader__.load({
1308
1508
  ' background: color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06)) 40%, transparent); }',
1309
1509
  '[data-dam-card] .dam-date { font-weight: 600; margin-bottom: 4px; }',
1310
1510
  '[data-dam-card] .dam-content { white-space: pre-wrap; word-break: break-word; max-height: 220px; overflow: auto; }',
1311
- '[data-dam-disclosure] { overflow: hidden; opacity: 0; max-height: 0; transform: translateY(-5px) scale(.985); transition: max-height .32s cubic-bezier(.22,.8,.2,1), opacity .2s ease, transform .28s cubic-bezier(.22,.8,.2,1); }',
1511
+ '[data-dam-disclosure] { overflow: hidden; opacity: 0; max-height: 0; transform: translateY(-5px) scale(.985); transition: max-height var(--dam-dur-fast) var(--dam-ease-out), opacity var(--dam-dur-fast) var(--dam-ease-out), transform var(--dam-dur-fast) var(--dam-ease-out); }',
1312
1512
  '[data-dam-disclosure][data-phase="open"] { opacity: 1; max-height: 1800px; transform: translateY(0) scale(1); }',
1313
1513
  '[data-dam-disclosure][data-phase="closing"] { opacity: 0; max-height: 0; transform: translateY(-5px) scale(.985); }',
1314
- '[data-dam-card], [data-dam-banner] { animation: dam-rise .34s cubic-bezier(.22,.8,.2,1) both; }',
1514
+ '[data-dam-card], [data-dam-banner] { animation: dam-rise var(--dam-dur-fast) var(--dam-ease-out) both; }',
1315
1515
  '@keyframes dam-rise { from { opacity: 0; transform: translateY(7px) scale(.988); } to { opacity: 1; transform: translateY(0) scale(1); } }',
1316
1516
  '@media (prefers-reduced-motion: reduce) { [data-dam-tab-strip], [data-dam-tab], [data-dam-disclosure], [data-dam-card], [data-dam-banner], [data-dam-tour-orb-wrap] *, [data-dam-update-box] * { transition: none !important; animation: none !important; }',
1317
1517
  ' [data-dam-tour-bokeh], [data-dam-tour-slab], [data-dam-tour-orb-core] { opacity: 1 !important; transform: none !important; filter: none !important; }',
1318
1518
  ' [data-dam-update-stage] { display: none !important; }',
1319
1519
  ' [data-dam-update-content] { opacity: 1 !important; transform: none !important; animation: none !important; } }',
1320
- '[data-dam-calendar] { animation: dam-rise .34s cubic-bezier(.22,.8,.2,1) both; }',
1321
- '[data-dam-calendar] [data-dam-calendar-day] { transition: transform .18s ease, border-color .2s ease, background .2s ease, box-shadow .2s ease; }',
1520
+ // ★2026-09-22 减动效闸门(第二道) —— MOTION-SPEC-20260922 §1.6:上面那道只覆盖 8 个选择器,
1521
+ // 漏了浮层/日历三件/页签交互/折叠头/侧栏/保存条。本批新增与改动的每一条动效都必须在两道里被归零。
1522
+ '@media (prefers-reduced-motion: reduce) {',
1523
+ ' [data-dam-panel], [data-dam-calendar], [data-dam-calendar-event], [data-dam-calendar-modal],',
1524
+ ' [data-dam-calendar] [data-dam-calendar-day], [data-dam-tabs-arrow], [data-dam-btn],',
1525
+ ' [data-dam-page-nav] button, [data-dam-page] [data-dam-fold], [data-dam-page] [data-dam-fold] .dam-fold-caret,',
1526
+ ' [data-dam-savebar], [data-dam-savebar] button { transition: none !important; animation: none !important; }',
1527
+ ' [data-dam-tab][data-active="true"], [data-dam-calendar] [data-dam-calendar-day]:hover,',
1528
+ ' [data-dam-page] [data-dam-flow] > * { animation: none !important; }',
1529
+ ' [data-dam-tab]:active, [data-dam-btn]:active, [data-dam-tabs-arrow]:active:not(:disabled) { transform: none !important; } }',
1530
+ '[data-dam-calendar] { animation: dam-rise var(--dam-dur-fast) var(--dam-ease-out) both; }',
1531
+ '[data-dam-calendar] [data-dam-calendar-day] { transition: transform var(--dam-dur-quick) var(--dam-ease-out), border-color .2s ease, background .2s ease, box-shadow .2s ease; }',
1322
1532
  '[data-dam-calendar] [data-dam-calendar-day]:hover { transform: translateY(-1px); border-color: var(--dam-accent, #1d4ed8) !important; box-shadow: 0 4px 12px rgba(0,0,0,.08); }',
1323
- '[data-dam-calendar-event] { animation: dam-event-in .25s cubic-bezier(.22,.8,.2,1) both; transition: transform .18s ease, filter .18s ease; }',
1533
+ '[data-dam-calendar-event] { animation: dam-event-in var(--dam-dur-fast) var(--dam-ease-out) both; transition: transform var(--dam-dur-quick) var(--dam-ease-out), filter var(--dam-dur-quick) var(--dam-ease-out); }',
1324
1534
  '[data-dam-calendar-event]:hover { transform: translateX(2px); filter: brightness(1.06); }',
1325
- '[data-dam-calendar-modal] { animation: dam-modal-in .26s cubic-bezier(.22,.8,.2,1) both; }',
1535
+ '[data-dam-calendar-modal] { animation: dam-modal-in var(--dam-dur-fast) var(--dam-ease-out) both; }',
1326
1536
  '@keyframes dam-event-in { from { opacity: 0; transform: translateX(-5px); } to { opacity: 1; transform: none; } }',
1327
1537
  '@keyframes dam-modal-in { from { opacity: 0; transform: translateY(10px) scale(.98); } to { opacity: 1; transform: none; } }',
1328
1538
  '[data-dam-banner] { border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #e6a23c) 55%, transparent);',
@@ -3027,12 +3237,15 @@ window.__ModuleLoader__.load({
3027
3237
  })
3028
3238
  ) : null,
3029
3239
  groups.map(function (grp) {
3030
- return h('div', { key: grp.day, style: { marginBottom: '16px' } },
3240
+ // ★大屏分列(2026-09-22):判定队列每天一组的卡片在宽屏排成多列。
3241
+ // CSS 规则是 `[data-dam-flow] > [data-dam-card]`(直接子元素) ⇒ 标记必须打在**分组容器**上,
3242
+ // 打在页签根上卡片不是直接子元素,只会退化成整行占位(无效接线)。
3243
+ return h('div', { key: grp.day, 'data-dam-flow': '', style: { marginBottom: '16px' } },
3031
3244
  h('div', { style: { fontSize: 'calc(11.5px * var(--dam-scale))', fontWeight: 700, opacity: .7, margin: '2px 2px 8px' } }, grp.day),
3032
3245
  grp.rows.map(function (row, i) {
3033
3246
  var rc = row.reasonCodes || []
3034
3247
  var del = row.delivery
3035
- return h('div', { key: row.observationId || i, style: card },
3248
+ return h('div', { key: row.observationId || i, 'data-dam-card': '', style: card },
3036
3249
  h('div', { style: { display: 'flex', alignItems: 'center', gap: '7px', marginBottom: '5px' } },
3037
3250
  (function () { var lk = row.lane === 'explicit' ? 'explicit' : 'proactive'; var c = laneBg[lk] || ['rgba(128,128,128,.16)', null]; return badge(locale === 'zh' ? (lk === 'explicit' ? '明确召回' : '主动观测') : lk, c[0], c[1]) })(),
3038
3251
  (function () { var dk = String(row.decision); var c = decBg[dk] || ['rgba(128,128,128,.14)', null]; return badge(dk.toUpperCase(), c[0], c[1]) })(),
@@ -3064,6 +3277,11 @@ window.__ModuleLoader__.load({
3064
3277
  var userTruncPair = useState(false)
3065
3278
  var userTruncated = userTruncPair[0]
3066
3279
  var setUserTruncated = userTruncPair[1]
3280
+ // ★2026-09-22:日志列表改折叠(用户要求「日志可以做成折叠的形式,可以展开」)。默认收起 ——
3281
+ // 首屏留给「硬性约束 + 用户笔记」这两块每轮无条件注入的稳定内容,日志属于翻查型内容。
3282
+ var logsOpenPair = useState(false)
3283
+ var logsOpen = logsOpenPair[0]
3284
+ var setLogsOpen = logsOpenPair[1]
3067
3285
  useEffect(function () {
3068
3286
  var alive = true
3069
3287
  apiGet(API.list).then(function (d) { if (alive) setData(d) }).catch(function () {})
@@ -3083,28 +3301,44 @@ window.__ModuleLoader__.load({
3083
3301
  h('div', { 'data-dam-content': '' }, content || t('empty')),
3084
3302
  h('button', { 'data-dam-btn': '', onClick: function () { setOpen(null); setContent('') } }, t('back'))))
3085
3303
  } else {
3086
- rows.push(h('div', { 'data-dam-hint': '' }, t('clickDateViewLog')))
3087
- for (var i = 0; i < data.logs.length; i++) {
3088
- (function (log) {
3089
- rows.push(h(Card, { title: log.date + ' · ' + fmtSize(log.size) },
3090
- h('button', { 'data-dam-btn': '', onClick: function () { setOpen(log.date + '.md') } }, t('view'))))
3091
- })(data.logs[i])
3092
- }
3093
- // 三级记忆之用户级:当日日志下方展示用户画像(跨项目规则/偏好)
3094
- rows.push(h('div', { 'data-dam-hint': '', style: { marginTop: '12px' } }, t('userMemoryBlock')))
3095
- rows.push(h(Card, { title: 'MEMORY.md (' + (locale === 'zh' ? '用户级' : 'user-level') + ')' },
3096
- h('div', { 'data-dam-content': '' }, userText || t('userMemoryEmpty')),
3097
- userTruncated ? h('div', { 'data-dam-hint': '' }, t('userMemoryTruncated')) : null))
3098
- // ★R7:硬性约束编辑面板(用户要求「能自行增删改」)
3099
- // ★2026-09-20 崩溃修复:本处原写 `{ nonce: nonce }`,但 **LogsTab 不接收任何 props**
3100
- // (`function LogsTab()`,调用点 `h(LogsTab)` 未传参),而 `nonce` 只在 MemoryPanel
3101
- // 作用域内定义(L3969-3970)⇒ 打开日志页签时求值 nonce 抛
3102
- // ReferenceError: nonce is not defined,整个窗格崩溃。
3103
- // RulesEditPanel 本就把它当可选(`props && props.nonce ? … : 0`),故传空对象即可。
3104
- rows.push(h(RulesEditPanel, {}))
3105
- }
3106
- return h('div', null, rows)
3304
+ // ★2026-09-22 版式调整(用户要求「把上面的那一堆东西和下面的对调」):
3305
+ // ① 硬性约束 与 用户笔记 提到最上面 —— 两者都是**每轮无条件注入**的稳定内容,使用者最该先看到;
3306
+ // ② 日志列表下移并改折叠 —— 日志是翻查型内容,不该占据首屏。
3307
+ // 两块都用 [data-dam-section] 包裹:它在 [data-dam-flow] 栅格里整行占位,不会被分列。
3308
+ rows.push(h('div', { 'data-dam-section': '' }, h(RulesEditPanel, {})))
3309
+ rows.push(h('div', { 'data-dam-section': '', style: { marginTop: '14px' } },
3310
+ h('div', { 'data-dam-hint': '', style: { marginBottom: '6px' } }, t('userMemoryBlock')),
3311
+ h(Card, { title: 'MEMORY.md (' + (locale === 'zh' ? '用户级' : 'user-level') + ')' },
3312
+ h('div', { 'data-dam-content': '' }, userText || t('userMemoryEmpty')),
3313
+ userTruncated ? h('div', { 'data-dam-hint': '' }, t('userMemoryTruncated')) : null)))
3314
+ rows.push(h('div', { 'data-dam-section': '', style: { marginTop: '14px' } }, h(FoldableLogs, {
3315
+ logs: data.logs || [], open: logsOpen, setOpen: setLogsOpen, onOpenFile: setOpen,
3316
+ })))
3317
+ }
3318
+ // 大屏分列:卡片流页签(见 CSS [data-dam-flow]);非卡片子元素仍整行。
3319
+ return h('div', { 'data-dam-flow': '' }, rows)
3107
3320
  }
3321
+ // ★2026-09-22 日志折叠块(从 LogsTab 抽出, 便于独立校验括号):标题可点, 内容用既有 AnimatedDisclosure。
3322
+ function FoldableLogs(props) {
3323
+ var open = !!props.open
3324
+ var list = props.logs || []
3325
+ return h('div', null,
3326
+ h('button', {
3327
+ 'data-dam-fold': '', 'data-open': open ? 'true' : 'false', 'aria-expanded': open ? 'true' : 'false',
3328
+ onClick: function () { props.setOpen(!open) },
3329
+ },
3330
+ h('span', { className: 'dam-fold-caret' }, '▸'),
3331
+ h('span', null, t('logs')),
3332
+ h('span', { className: 'dam-fold-count' }, list.length + (locale === 'zh' ? ' 篇' : ' files'))),
3333
+ h(AnimatedDisclosure, { open: open },
3334
+ h('div', { style: { paddingTop: '8px' } },
3335
+ h('div', { 'data-dam-hint': '' }, t('clickDateViewLog')),
3336
+ list.map(function (log) {
3337
+ return h(Card, { key: log.date, title: log.date + ' · ' + fmtSize(log.size) },
3338
+ h('button', { 'data-dam-btn': '', onClick: function () { props.onOpenFile(log.date + '.md') } }, t('view')))
3339
+ }))))
3340
+ }
3341
+
3108
3342
  function pathName(p) { var parts = String(p).split(/[\\/]/); return parts[parts.length - 1] }
3109
3343
 
3110
3344
  // ── ★R1–R6(2026-09-20 用户「看不懂只能盲确认」)审批面人话化辅助 ────────
@@ -3132,6 +3366,14 @@ window.__ModuleLoader__.load({
3132
3366
  if (c === 'model-authorized') continue
3133
3367
  }
3134
3368
  if (promotion.decision === 'promote') return t('hubWhyCanPromote')
3369
+ // ★C(2026-09-22):把「关在哪一类门」说透 —— 统计门可人工越过,结构门不可。
3370
+ // 没有这一段时,界面上"一个按钮都没有",用户只能猜(这正是"手动通道被关掉"的观感来源)。
3371
+ if (promotion.decision === 'keep') {
3372
+ var head = codes.length ? codes.join(' · ') : ''
3373
+ if (promotion.overridable === true) return head + ' | ' + t('hubWhyOverridable')
3374
+ if (promotion.gateKind === 'structural') return head + ' | ' + t('hubWhyStructural')
3375
+ return head
3376
+ }
3135
3377
  return codes.length ? codes.join(' · ') : ''
3136
3378
  }
3137
3379
 
@@ -3327,7 +3569,29 @@ window.__ModuleLoader__.load({
3327
3569
  h('div', { style: { display: 'flex', gap: '6px', marginTop: '5px' } },
3328
3570
  // issue #30:纯 episode 观察行**结构上**不可能晋升(无 successCriteria),
3329
3571
  // 继续显示"晋升"按钮只会让人反复点击却看不到变化 ⇒ 直接隐藏并打观察标。
3330
- !p.observationOnly && h('button', { 'data-dam-btn': '', style: { fontSize: 'calc(10.5px * var(--dam-scale))', padding: '3px 10px' }, onClick: function () { hubAct('promote', p.procedureId) } }, locale === 'zh' ? '晋升' : 'promote'),
3572
+ // ★ 2026-09-21 bugfix(A-9):原条件只挡 observationOnly,与 store 真实门限**不一致** ——
3573
+ // 缺 successCriteria / 有 correction / correctionRate 超限的条目照样显示按钮,
3574
+ // 用户点下去只得到一行 keep 灰字,感受即「批准不了」。
3575
+ // 现改为**直接依赖只读投影** `p.promotion`(宿主 memory-hub 用 evaluatePromotion 生成,
3576
+ // 纯只读、不写盘):decision==='promote' 才给按钮,否则界面已由 R2 区块说明原因。
3577
+ // fail-soft:投影缺失(旧宿主/异常)时退回原 observationOnly 判据,不做过度拦截。
3578
+ (!p.observationOnly && (!p.promotion || p.promotion.decision === 'promote'))
3579
+ && h('button', { 'data-dam-btn': '', style: { fontSize: 'calc(10.5px * var(--dam-scale))', padding: '3px 10px' }, onClick: function () { hubAct('promote', p.procedureId) } }, locale === 'zh' ? '晋升' : 'promote'),
3580
+ // ★2026-09-22 补接线(前端接线审计 P0-1):宿主已提供 approve 原语
3581
+ // (index.js 白名单含 action:'approve' + procedure-store-pre approve('user')),
3582
+ // 而上面「晋升」按钮门控在 decision==='promote' ⇒ 高风险条目(decision==='ask')
3583
+ // 在界面上是死路:用户看得到「待人工批准」却没有任何按钮可点。
3584
+ (p.promotion && p.promotion.decision === 'ask')
3585
+ && h('button', { 'data-dam-btn': '', style: { fontSize: 'calc(10.5px * var(--dam-scale))', padding: '3px 10px', fontWeight: 650 }, onClick: function () { hubAct('approve', p.procedureId) } }, locale === 'zh' ? '批准(人工)' : 'approve (manual)'),
3586
+ // ★B/C(2026-09-22):人工**强制晋升** —— 只在「拦它的门**全是统计门**」时给出。
3587
+ // 判据来自宿主只读投影 p.promotion.overridable(memory-hub 用 promotionOverrideView 生成,
3588
+ // 只读、零副作用、且**不重复实现门限** ⇒ 结构上不可能与 promote() 漂移)。
3589
+ // 结构门拦(已弃用 / 观察型 / 无 successCriteria / 有纠正记录)时**不给按钮** ——
3590
+ // 否则就是再造一个「点了没反应」的假通道(A-9 收掉旧按钮的正是这个原因)。
3591
+ // 动作走 index.js 白名单 `force-promote` → promote(pid, {}, { authorizedBy:'user' }),
3592
+ // 只越过 diversity/success 两道统计门,并在条目上留 `authorizedBy:'user'` 痕迹。
3593
+ (p.promotion && p.promotion.overridable === true)
3594
+ && h('button', { 'data-dam-btn': '', title: t('hubForcePromoteTitle'), style: { fontSize: 'calc(10.5px * var(--dam-scale))', padding: '3px 10px', fontWeight: 650 }, onClick: function () { hubAct('force-promote', p.procedureId) } }, t('hubForcePromote')),
3331
3595
  !p.observationOnly && p.stage === 'validated' && h('button', { 'data-dam-btn': '', style: { fontSize: 'calc(10.5px * var(--dam-scale))', padding: '3px 10px' }, onClick: function () { hubAct('activate', p.procedureId) } }, locale === 'zh' ? '直接激活' : 'activate'),
3332
3596
  h('button', { 'data-dam-btn': '', style: { fontSize: 'calc(10.5px * var(--dam-scale))', padding: '3px 10px', opacity: .75 }, onClick: function () { hubAct('deprecate', p.procedureId) } }, locale === 'zh' ? '弃用' : 'deprecate'),
3333
3597
  h('button', { 'data-dam-btn': '', style: { fontSize: 'calc(10.5px * var(--dam-scale))', padding: '3px 10px', opacity: .75 }, onClick: function () { hubAct('pin', p.procedureId, !p.pinned) } }, p.pinned ? (locale === 'zh' ? '取消置顶' : 'unpin') : (locale === 'zh' ? '置顶' : 'pin'))))
@@ -3341,6 +3605,18 @@ window.__ModuleLoader__.load({
3341
3605
  factList.length ? factList.slice(0, 6).map(function (f) {
3342
3606
  return h('div', { 'data-dam-content': '', key: f.factId, style: { padding: '3px 0' } }, f.subject + ' · ' + f.predicate + (f.object ? ' · ' + f.object : ''))
3343
3607
  }) : h('div', { 'data-dam-content': '' }, t('hubFactsEmpty'))))
3608
+ // ★P0-3a 补接线(2026-09-22):事实保留上限淘汰台账。宿主早已把它放进 /memory-hub 应答
3609
+ // (memory-hub.js:245 → stores.facts.getStats()),前端此前零渲染
3610
+ // ⇒「事实被悄悄淘汰了」在界面上无处可查。pruneProtected>0 表示已轮到删「重要项」⇒ 上限偏低。
3611
+ var factStats = (facts && facts.stats) || null
3612
+ if (factStats && (Number(factStats.pruned) > 0 || Number(factStats.pruneProtected) > 0)) {
3613
+ var fpPruned = Number(factStats.pruned) || 0
3614
+ var fpProtected = Number(factStats.pruneProtected) || 0
3615
+ rows.push(h('div', { 'data-dam-hint': '', style: { marginTop: '6px', color: fpProtected > 0 ? 'var(--dsw-alias-state-warn-primary, #e8c584)' : undefined } },
3616
+ locale === 'zh'
3617
+ ? '已按保留上限淘汰 ' + fpPruned + ' 条事实' + (fpProtected > 0 ? ',其中 ' + fpProtected + ' 条属重要/未撤销条目 —— 上限偏低,建议到 设置 → 自动记忆引擎 调高「事实保留上限」' : '')
3618
+ : fpPruned + ' fact(s) evicted by the retention cap' + (fpProtected > 0 ? ', incl. ' + fpProtected + ' important/unrevoked — cap too low; raise "Fact retention limit" under Settings → Semantic engine' : '')))
3619
+ }
3344
3620
  if (facts && facts.pendingConflicts && facts.pendingConflicts.length) {
3345
3621
  rows.push(h('div', { 'data-dam-hint': '', style: { color: 'var(--dsw-alias-state-warn-primary, #e8c584)' } }, t('hubConflicts') + ': ' + facts.pendingConflicts.length))
3346
3622
  }
@@ -3355,7 +3631,8 @@ window.__ModuleLoader__.load({
3355
3631
  // 统计
3356
3632
  rows.push(h('div', { 'data-dam-hint': '', style: { marginTop: '12px', opacity: .7 } },
3357
3633
  (locale === 'zh' ? '中枢统计: ' : 'Hub stats: ') + (data.stats ? JSON.stringify(data.stats) : '')))
3358
- return h('div', null, rows)
3634
+ // 大屏分列:卡片流页签(见 CSS [data-dam-flow]);非卡片子元素仍整行。
3635
+ return h('div', { 'data-dam-flow': '' }, rows)
3359
3636
  }
3360
3637
 
3361
3638
  // M10 存储管理页签(2026-08-30 P3):数据源=/storage-manage(loopback 只读投影 + 三类动作)。
@@ -3444,7 +3721,8 @@ window.__ModuleLoader__.load({
3444
3721
  rows.push(h('div', { 'data-dam-hint': '', style: { marginTop: '12px', opacity: .7 } },
3445
3722
  (locale === 'zh' ? '最近动作: ' : 'recent: ') + audit.map(function (a) { return a.action }).join(', ')))
3446
3723
  }
3447
- return h('div', null, rows)
3724
+ // 大屏分列:卡片流页签(见 CSS [data-dam-flow]);非卡片子元素仍整行。
3725
+ return h('div', { 'data-dam-flow': '' }, rows)
3448
3726
  }
3449
3727
 
3450
3728
  function NotesTab() {
@@ -3872,7 +4150,8 @@ window.__ModuleLoader__.load({
3872
4150
  }
3873
4151
  // 2026-09-14 修复:开关卡此前只在「未启用」分支被返回 ⇒ 白板一旦开启,白板页就再也看不到开关(开得了、关不掉)。
3874
4152
  // 开关卡必须在**两种**状态下都渲染 —— 它是这个页面上唯一的开关入口。
3875
- return h('div', null, [switchCard].concat(rows))
4153
+ // 大屏分列:卡片流页签(见 CSS [data-dam-flow]);非卡片子元素仍整行。
4154
+ return h('div', { 'data-dam-flow': '' }, [switchCard].concat(rows))
3876
4155
  }
3877
4156
 
3878
4157
  function ReflectionsTab() {
@@ -3928,7 +4207,8 @@ window.__ModuleLoader__.load({
3928
4207
  })(data.reflections[i])
3929
4208
  }
3930
4209
  }
3931
- return h('div', null, rows)
4210
+ // 大屏分列:卡片流页签(见 CSS [data-dam-flow]);非卡片子元素仍整行。
4211
+ return h('div', { 'data-dam-flow': '' }, rows)
3932
4212
  }
3933
4213
 
3934
4214
  function SearchTab() {
@@ -3956,7 +4236,8 @@ window.__ModuleLoader__.load({
3956
4236
  apiPost(API.smartRecall, { query: q.trim() }).then(function (d) { setSmart(d); setBusy(false) })
3957
4237
  .catch(function (e) { setSmart({ answer: t('searchFailed') + e.message, keywords: [], hits: [] }); setBusy(false) })
3958
4238
  }
3959
- return h('div', null,
4239
+ // 大屏分列:卡片流页签(见 CSS [data-dam-flow]);非卡片子元素仍整行。
4240
+ return h('div', { 'data-dam-flow': '' },
3960
4241
  h('div', { 'data-dam-row': '' },
3961
4242
  h('input', { 'data-dam-input': '', placeholder: t('searchPlaceholder'), value: q, onChange: function (e) { setQ(e.target.value) }, onKeyDown: function (e) { if (e.key === 'Enter') search() } }),
3962
4243
  h('button', { 'data-dam-btn': '', onClick: search, disabled: busy }, t('searchBtn')),
@@ -3966,7 +4247,16 @@ window.__ModuleLoader__.load({
3966
4247
  h('div', { 'data-dam-content': '' }, smart.answer || t('empty')),
3967
4248
  smart.keywords && smart.keywords.length ? h('div', { 'data-dam-hint': '', style: { marginTop: '6px' } }, t('keywordsLabel') + smart.keywords.join(' / ')) : null,
3968
4249
  Array.isArray(smart.hits) && smart.hits.length ? h('div', { 'data-dam-hint': '', style: { marginTop: '6px' } },
3969
- smart.hits.map(function (hit, i) { return hit && typeof hit === 'object' ? h('div', { key: i, style: { marginTop: '3px', overflowWrap: 'anywhere' } }, '· [' + String(hit.where || '') + '] ' + String(hit.line || '')) : null })) : null)
4250
+ // ★P1-2 补接线(2026-09-22):宿主**早已**把结论层标记放进 smartRecall 的 hits 投影
4251
+ // (index.js:6469 → l0-extract.js:295+,文案形如「⚠已撤回」),前端此前零渲染
4252
+ // ⇒「这条结论已失效」在界面上完全看不出来,用户会把它当有效结论读。mark 自带文案 ⇒ 零 i18n。
4253
+ smart.hits.map(function (hit, i) {
4254
+ if (!hit || typeof hit !== 'object') return null
4255
+ var mark = hit.mark === undefined || hit.mark === null ? '' : String(hit.mark).trim()
4256
+ return h('div', { key: i, style: { marginTop: '3px', overflowWrap: 'anywhere' } },
4257
+ mark ? h('span', { 'data-dam-hint': '', style: { marginRight: '5px', fontWeight: 600, color: 'var(--dsw-alias-state-warn-primary, #e8c584)' } }, mark) : null,
4258
+ '· [' + String(hit.where || '') + '] ' + String(hit.line || ''))
4259
+ })) : null)
3970
4260
  : null,
3971
4261
  result ? h(Card, { title: t('resultTitle') }, h('div', { 'data-dam-content': '' }, result)) : null)
3972
4262
  }
@@ -4088,7 +4378,8 @@ window.__ModuleLoader__.load({
4088
4378
  h('div', { 'data-dam-graph-toolbar': '' }, h('span', { 'data-dam-hint': '' }, locale === 'zh' ? '思维导图大小' : 'Map size'), h('input', { type: 'range', min: '0.55', max: '1.75', step: '0.05', value: graphScale, 'aria-label': locale === 'zh' ? '调整思维导图大小' : 'Adjust map size', onChange: function (e) { setGraphScale(Number(e.target.value) || 1.0) } }), h('span', { 'data-dam-hint': '' }, Math.round(graphScale * 100) + '%'), h('button', { 'data-dam-btn': '', title: locale === 'zh' ? '重置视图' : 'Reset view', onClick: function () { setGraphScale(1.0) } }, '⌂')),
4089
4379
  h('div', { 'data-dam-hint': '' }, locale === 'zh' ? '点击工作区卡片查看摘要;拖动滑块调整图的大小。' : 'Select a workspace card for details; use the slider to resize the map.'),
4090
4380
  h(WorkspaceGraph, { workspaces: workspaces, graph: data.graph, scale: graphScale, onSelect: function (ws) { setSelected(ws); var n = Object.assign({}, collapsedByPath); n[ws.path] = false; setCollapsedByPath(n) } }),
4091
- h('div', { style: { marginTop: '12px' } }, workspaces.map(function (ws) {
4381
+ // ★大屏分列(2026-09-22):工作区卡片(已带 [data-dam-card])在宽屏排成多列。
4382
+ h('div', { 'data-dam-flow': '', style: { marginTop: '12px' } }, workspaces.map(function (ws) {
4092
4383
  var open = collapsedByPath[ws.path] === false
4093
4384
  return h('div', { key: ws.path, 'data-dam-card': '', style: { marginBottom: '8px', padding: '0' } },
4094
4385
  h('button', { 'data-dam-btn': '', 'aria-expanded': open ? 'true' : 'false', onClick: function () { var n = Object.assign({}, collapsedByPath); n[ws.path] = !open; setCollapsedByPath(n); setSelected(ws) }, style: { width: '100%', textAlign: 'left', padding: '10px 12px', fontWeight: 650 } }, (open ? '▾ ' : '▸ ') + ws.name + (ws.dateRange ? ' · ' + ws.dateRange : '')),
@@ -4438,7 +4729,9 @@ window.__ModuleLoader__.load({
4438
4729
  : null)))
4439
4730
  })(sources[i])
4440
4731
  }
4441
- return h('div', null,
4732
+ // ★大屏分列(2026-09-22):本组件的 Card 由 `cards)` 直接铺进根节点 ⇒ 是根的**直接子元素**,
4733
+ // 在根上打 [data-dam-flow] 即可生效(与日志/中枢页签同款)。非卡片子元素(hint/按钮行/msg/err)仍整行。
4734
+ return h('div', { 'data-dam-flow': '' },
4442
4735
  h('div', { 'data-dam-hint': '' }, t('connectHint')),
4443
4736
  h('div', { 'data-dam-row': '' },
4444
4737
  h('button', { 'data-dam-btn': '', onClick: importAll }, t('importAll')),
@@ -4524,12 +4817,97 @@ window.__ModuleLoader__.load({
4524
4817
  h('button', { 'data-dam-tabs-arrow': '', title: locale === 'zh' ? '向右滚动模块' : 'Scroll modules right', 'aria-label': locale === 'zh' ? '向右滚动模块' : 'Scroll modules right', disabled: !canRight, onClick: function () { step(1) } }, '›'))
4525
4818
  }
4526
4819
 
4820
+ // ══════════════════════════════════════════════════════════════════════════
4821
+ // 共享渲染件(2026-09-21):浮层与「记忆」会话页复用同一份页签表与内容体。
4822
+ // 抽出来的动机:两种承载面必须"同内容、同页签、同数据"(用户要求同步),
4823
+ // 若各写一份 body 分支,页签一多必然漂移 ⇒ 只留一处事实。
4824
+ // ══════════════════════════════════════════════════════════════════════════
4825
+ function MemoryTabBody(tab, nonce) {
4826
+ if (tab === 'overview') return h(OverviewTab, { nonce: nonce })
4827
+ if (tab === 'logs') return h(LogsTab)
4828
+ if (tab === 'refine') return h(RefineTab)
4829
+ if (tab === 'hub') return h(MemoryHubTab, { nonce: nonce })
4830
+ if (tab === 'storage') return h(StorageTab, { nonce: nonce })
4831
+ if (tab === 'notes') return h(NotesTab)
4832
+ if (tab === 'plan') return h(PlanTab)
4833
+ if (tab === 'reflections') return h(ReflectionsTab)
4834
+ if (tab === 'connect') return h(ConnectTab)
4835
+ if (tab === 'calendar') return h(CalendarTab)
4836
+ if (tab === 'workspaces') return h(WorkspaceTab)
4837
+ return h(SearchTab)
4838
+ }
4839
+ function MEMORY_TABS() {
4840
+ return [['overview', t('overview')], ['logs', t('logs')], ['refine', t('refineTab')], ['hub', t('hubTab')], ['storage', t('storageTab')], ['notes', t('notes')], ['plan', t('planTab')], ['reflections', t('reflections')], ['connect', t('connect')], ['calendar', t('calendar')], ['search', t('search')], ['workspaces', t('workspaces')]]
4841
+ }
4842
+
4843
+ // 「记忆」会话页(2026-09-21 二次修正) —— 注册在 conversation.view 槽位,与「对话轨迹」「上下文」「白板看板」并列。
4844
+ // 用户诉求原话:「它现在是浮在整个页面上方的,而不是和对话轨迹、上下文、白板看板在一起,作为一个单独的一页」。
4845
+ // 宿主只给 shell.overlay 这类"盖在界面上"的槽位,做不出并列页 ⇒ 必须走 conversation.view。
4846
+ // 与浮层的关系:同一份 panelTab/数据/刷新 nonce,切页签两边同步;只在承载面上不同(页更宽)。
4847
+ function MemoryPageView(props) {
4848
+ var tick = useTick()
4849
+ var tab = controller.panelTab()
4850
+ var noncePair = useState(0)
4851
+ var nonce = noncePair[0]
4852
+ var setNonce = noncePair[1]
4853
+ var verPair = useState(null)
4854
+ var ver = verPair[0]
4855
+ var setVer = verPair[1]
4856
+ useEffect(function () { return controller.subscribe(tick[1]) }, [])
4857
+ useEffect(function () {
4858
+ var alive = true
4859
+ apiGet(API.updateCheck).then(function (d) { if (alive && d && d.current) setVer(d.current) }).catch(function () {})
4860
+ return function () { alive = false }
4861
+ }, [])
4862
+ var zh = locale === 'zh'
4863
+ var verBadge = ver ? 'v' + ver + '-ui' : ''
4864
+ var body = MemoryTabBody(tab, nonce)
4865
+ // 浮层开关(page ↔ both 互切):两个方向都不注销本页签(修自毁按钮)。
4866
+ var floatOn = controller.panelPos() === 'both'
4867
+ var floatLabel = floatOn ? (zh ? '收起浮层' : 'Hide card') : (zh ? '打开浮层' : 'Open card')
4868
+ var floatTitle = floatOn ? (zh ? '收起左下角浮层(保留本页)' : 'Hide bottom-left card (keep this page)') : (zh ? '同时打开左下角浮层(保留本页)' : 'Also open bottom-left card (keep this page)')
4869
+ var onFloatToggle = function () { controller.setPanelPos(floatOn ? 'page' : 'both') }
4870
+ var scaleAttr = fontScale in FONT_SCALES ? fontScale : 'md'
4871
+ return h('div', {
4872
+ 'data-dam-page': '',
4873
+ 'data-scale': scaleAttr,
4874
+ style: {
4875
+ '--dam-user-scale': FONT_SCALE_VALUES[fontScale] || '1',
4876
+ '--dam-accent': ACCENT_VALUES[accentTheme] || ACCENT_VALUES.deepseek,
4877
+ },
4878
+ },
4879
+ h('header', { 'data-dam-page-head': '' },
4880
+ h('strong', null, t('autoMemory')),
4881
+ h('span', { 'data-dam-hint': '', style: { fontSize: '10px', opacity: .55 } }, verBadge),
4882
+ h('span', { className: 'dam-spacer' }),
4883
+ h('button', { 'data-dam-btn': '', title: t('refresh'), onClick: function () { setNonce(nonce + 1) } }, '⟳'),
4884
+ // ★2026-09-22 修「自毁按钮」(方向文档 §3.1 硬性要求 1):原实现在 page 档点一下即把本页签注销
4885
+ // (conversation.view 仅在 page/both 档注册)⇒ 用户实测「上面的东西连按钮一起消失」。
4886
+ // 现改为 page ↔ both 互切:both 档页签始终保留,两个方向都不注销当前页。
4887
+ h('button', {
4888
+ 'data-dam-btn': '', 'data-dam-float-toggle': floatOn ? 'on' : 'off',
4889
+ title: floatTitle,
4890
+ onClick: onFloatToggle,
4891
+ }, floatLabel)),
4892
+ // ★2026-09-22 版式:顶部横向滚动页签 → 左侧固定侧栏(用户原话「把原本放在上面的那个切换横条
4893
+ // 放到左侧,这样排版会更科学一点」)。浮层仍用 TabScroller(窄容器保持原版式),
4894
+ // 两者共享同一份 panelTab 状态 ⇒ 切分区两边同步(既有契约不变)。
4895
+ h('div', { 'data-dam-page-main': '' },
4896
+ h('nav', { 'data-dam-page-nav': '', 'aria-label': zh ? '记忆分区' : 'Memory sections' },
4897
+ MEMORY_TABS().map(function (item) {
4898
+ return h('button', { key: item[0], 'data-dam-nav-item': '', 'data-active': tab === item[0] ? 'true' : undefined, onClick: function () { controller.setPanelTab(item[0]) } }, item[1])
4899
+ })),
4900
+ h('div', { 'data-dam-page-content': '' },
4901
+ h('div', { 'data-dam-body': '' }, body))))
4902
+ }
4903
+
4527
4904
  function MemoryPanel() {
4528
4905
  var tick = useTick()
4529
4906
  var panelRef = useRef(null)
4530
- var tabPair = useState('overview')
4531
- var tab = tabPair[0]
4532
- var setTab = tabPair[1]
4907
+ // 页签状态走模块级共享(2026-09-21):浮层与「记忆」会话页读同一个 panelTab ⇒ 两处切页互相同步。
4908
+ // (用户诉求原话:「两者共存…要保证同步」。不用两份 useState,从根上杜绝状态漂移。)
4909
+ var tab = controller.panelTab()
4910
+ var setTab = controller.setPanelTab
4533
4911
  // 刷新 nonce:每次打开面板 / 点 ⟳ 时递增,驱动各页签重拉数据
4534
4912
  var noncePair = useState(0)
4535
4913
  var nonce = noncePair[0]
@@ -4538,6 +4916,7 @@ window.__ModuleLoader__.load({
4538
4916
  var ver = verPair[0]
4539
4917
  var setVer = verPair[1]
4540
4918
  var g = controller.geom()
4919
+ var pos = controller.panelPos()
4541
4920
  useEffect(function () { return controller.subscribe(tick[1]) }, [])
4542
4921
  // 真实版本号(#15):面板头徽标不再硬编码,从 update-check 的 current(读包 version)取,失败留空
4543
4922
  useEffect(function () {
@@ -4549,81 +4928,91 @@ window.__ModuleLoader__.load({
4549
4928
  // 主题令牌 --dsw-alias-bg-layer-2 本身就是半透明的,面板文字几乎不可读。判别信号用
4550
4929
  // 「令牌自身的 alpha」(增强模式显著低于普通模式的 0.86-0.9):< 0.65 时把面板背景提升到
4551
4930
  // 0.96(保留色相)并弱化顶部高光。普通模式令牌不透明 → 不触发,液态玻璃观感零变化。
4931
+ // 2026-09-21:两种形态(浮层/顶部停靠条)各查一次 —— panelRef 只绑浮层,停靠条用选择器兜底,
4932
+ // 否则增强模式下顶栏模式会保持半透明、可读性退化。
4552
4933
  useEffect(function () {
4553
4934
  if (!panelOpen) return
4554
- var el = panelRef.current
4555
- if (!el) return
4556
- try {
4557
- var cs = getComputedStyle(el)
4558
- var token = cs.getPropertyValue('--dsw-alias-bg-layer-2')
4559
- var col = parseCssColor((token || '').trim() || cs.backgroundColor)
4560
- if (col && col.a < 0.65) {
4561
- el.style.background = 'rgba(' + col.r + ', ' + col.g + ', ' + col.b + ', 0.96)'
4562
- el.setAttribute('data-solid', 'true')
4563
- } else {
4564
- el.style.background = ''
4565
- el.removeAttribute('data-solid')
4566
- }
4567
- } catch (e) {}
4935
+ // 2026-09-21 二次修正:原「顶部停靠条」第二形态已废弃(改走 conversation.view 会话页),
4936
+ // 故此处只剩浮层一个元素需要兜底 —— 会话页在正常文档流里,背景由宿主给,无需提升不透明度。
4937
+ var els = panelRef.current ? [panelRef.current] : []
4938
+ for (var j = 0; j < els.length; j++) {
4939
+ var el = els[j]
4940
+ if (!el) continue
4941
+ try {
4942
+ var cs = getComputedStyle(el)
4943
+ var token = cs.getPropertyValue('--dsw-alias-bg-layer-2')
4944
+ var col = parseCssColor((token || '').trim() || cs.backgroundColor)
4945
+ if (col && col.a < 0.65) {
4946
+ el.style.background = 'rgba(' + col.r + ', ' + col.g + ', ' + col.b + ', 0.96)'
4947
+ el.setAttribute('data-solid', 'true')
4948
+ } else {
4949
+ el.style.background = ''
4950
+ el.removeAttribute('data-solid')
4951
+ }
4952
+ } catch (eEl) {}
4953
+ }
4568
4954
  }, [panelOpen])
4569
4955
  if (!panelOpen && !panelClosing) return null
4570
- var body
4571
- if (tab === 'overview') body = h(OverviewTab, { nonce: nonce })
4572
- else if (tab === 'logs') body = h(LogsTab)
4573
- else if (tab === 'refine') body = h(RefineTab)
4574
- else if (tab === 'hub') body = h(MemoryHubTab, { nonce: nonce })
4575
- else if (tab === 'storage') body = h(StorageTab, { nonce: nonce })
4576
- else if (tab === 'notes') body = h(NotesTab)
4577
- else if (tab === 'plan') body = h(PlanTab)
4578
- else if (tab === 'reflections') body = h(ReflectionsTab)
4579
- else if (tab === 'connect') body = h(ConnectTab)
4580
- else if (tab === 'calendar') body = h(CalendarTab)
4581
- else if (tab === 'workspaces') body = h(WorkspaceTab)
4582
- else body = h(SearchTab)
4583
- var tabs = [['overview', t('overview')], ['logs', t('logs')], ['refine', t('refineTab')], ['hub', t('hubTab')], ['storage', t('storageTab')], ['notes', t('notes')], ['plan', t('planTab')], ['reflections', t('reflections')], ['connect', t('connect')], ['calendar', t('calendar')], ['search', t('search')], ['workspaces', t('workspaces')]]
4584
- var style = {
4585
- left: g.left + 'px',
4586
- top: g.top + 'px',
4587
- width: g.width + 'px',
4588
- height: g.height + 'px',
4589
- '--dam-scale': FONT_SCALE_VALUES[fontScale] || '1',
4590
- '--dam-accent': ACCENT_VALUES[accentTheme] || ACCENT_VALUES.deepseek,
4591
- }
4956
+ var body = MemoryTabBody(tab, nonce)
4957
+ var tabs = MEMORY_TABS()
4958
+ // 承载面(2026-09-21):①左下角浮层(可拖拽/缩放) ②「记忆」会话页(conversation.view,与「对话轨迹」并列)。
4959
+ // 两者共用同一份 tab(模块级 panelTab)/数据/刷新 nonce ⇒ 天然同步(用户要求);本函数只负责浮层节点。
4592
4960
  var dragMove = startPointerDrag(function (dx, dy) {
4593
4961
  controller.setGeom({ left: g.left + dx, top: g.top + dy })
4594
4962
  }, '[data-dam-btn], [data-dam-tab], [data-dam-input], [data-dam-select], textarea')
4595
4963
  var resizeMove = startPointerDrag(function (dx, dy) {
4596
4964
  controller.setGeom({ width: g.width + dx, height: g.height + dy })
4597
4965
  })
4598
- return h('div', {
4599
- 'data-dam-panel': '',
4600
- 'data-scale': fontScale in FONT_SCALES ? fontScale : 'md',
4601
- ref: panelRef,
4602
- style: style,
4603
- 'data-closing': panelClosing ? 'true' : undefined,
4604
- 'data-dragging': dragActive ? 'true' : undefined,
4605
- },
4606
- h('header', {
4607
- title: t('dragMove'),
4608
- onPointerDown: dragMove,
4966
+ var scaleAttr = fontScale in FONT_SCALES ? fontScale : 'md'
4967
+ var verBadge = ver ? 'v' + ver + '-ui' : ''
4968
+ var nodes = []
4969
+ if (pos === 'bottom-left' || pos === 'both') {
4970
+ var floatStyle = {
4971
+ left: g.left + 'px',
4972
+ top: g.top + 'px',
4973
+ width: g.width + 'px',
4974
+ height: g.height + 'px',
4975
+ '--dam-scale': FONT_SCALE_VALUES[fontScale] || '1',
4976
+ '--dam-accent': ACCENT_VALUES[accentTheme] || ACCENT_VALUES.deepseek,
4977
+ }
4978
+ nodes.push(h('div', {
4979
+ key: 'float',
4980
+ 'data-dam-panel': '',
4981
+ 'data-pos': 'float',
4982
+ 'data-scale': scaleAttr,
4983
+ ref: panelRef,
4984
+ style: floatStyle,
4985
+ 'data-closing': panelClosing ? 'true' : undefined,
4986
+ 'data-dragging': dragActive ? 'true' : undefined,
4609
4987
  },
4610
- h('strong', null, t('autoMemory')),
4611
- h('span', { 'data-dam-hint': '', style: { fontSize: '10px', opacity: .55 } }, ver ? 'v' + ver + '-ui' : ''),
4612
- h('span', { className: 'dam-spacer' }),
4613
- // 悬浮钉(2026-09-08):钉住后点面板外不再自动收起;图标为线描 SVG,与 ⤾ ⟳ ✕ 同画风
4614
- h('button', {
4615
- 'data-dam-btn': '', 'data-pin': pinned ? 'on' : 'off',
4616
- title: pinned ? t('unpinPanel') : t('pinPanel'),
4617
- 'aria-pressed': pinned ? 'true' : 'false',
4618
- style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: '4px 6px' },
4619
- onClick: function () { controller.togglePin() },
4620
- }, h(PinIcon, { filled: pinned })),
4621
- h('button', { 'data-dam-btn': '', title: t('resetPos'), onClick: function () { controller.resetGeom() } }, '⤾'),
4622
- h('button', { 'data-dam-btn': '', title: t('refresh'), onClick: function () { setNonce(nonce + 1) } }, '⟳'),
4623
- h('button', { 'data-dam-btn': '', title: t('close'), onClick: function () { controller.close() } }, '✕')),
4624
- h(TabScroller, { tabs: tabs, tab: tab, setTab: setTab }),
4625
- h('div', { 'data-dam-body': '' }, body),
4626
- h('div', { 'data-dam-resize': '', title: t('dragResize'), onPointerDown: resizeMove }))
4988
+ h('header', {
4989
+ title: t('dragMove'),
4990
+ onPointerDown: dragMove,
4991
+ },
4992
+ h('strong', null, t('autoMemory')),
4993
+ h('span', { 'data-dam-hint': '', style: { fontSize: '10px', opacity: .55 } }, verBadge),
4994
+ h('span', { className: 'dam-spacer' }),
4995
+ // 悬浮钉(2026-09-08):钉住后点面板外不再自动收起;图标为线描 SVG,与 ⤾ ⟳ ✕ 同画风
4996
+ h('button', {
4997
+ 'data-dam-btn': '', 'data-pin': pinned ? 'on' : 'off',
4998
+ title: pinned ? t('unpinPanel') : t('pinPanel'),
4999
+ 'aria-pressed': pinned ? 'true' : 'false',
5000
+ style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: '4px 6px' },
5001
+ onClick: function () { controller.togglePin() },
5002
+ }, h(PinIcon, { filled: pinned })),
5003
+ h('button', { 'data-dam-btn': '', title: t('resetPos'), onClick: function () { controller.resetGeom() } }, '⤾'),
5004
+ h('button', { 'data-dam-btn': '', title: t('refresh'), onClick: function () { setNonce(nonce + 1) } }, '⟳'),
5005
+ h('button', { 'data-dam-btn': '', title: t('close'), onClick: function () { controller.close() } }, '✕')),
5006
+ h(TabScroller, { key: 'ftabs', tabs: tabs, tab: tab, setTab: setTab }),
5007
+ h('div', { key: 'fbody', 'data-dam-body': '' }, body),
5008
+ h('div', { 'data-dam-resize': '', title: t('dragResize'), onPointerDown: resizeMove })))
5009
+ }
5010
+ // ★2026-09-21 二次修正:「顶部通栏覆盖条」形态已**废弃**。
5011
+ // 用户实测反馈原话:「它现在是浮在整个页面上方的,而不是和对话轨迹、上下文、白板看板在一起,作为一个单独的一页」。
5012
+ // 根因:宿主只给插件 4 个槽位(sidebar/main/rightbar/shell.overlay),放在 overlay 里的东西**只能盖在界面上**,
5013
+ // 做不出「并列的一页」;要成为并列页必须注册到 conversation.view(见下面 MemoryPageView + registerSurfaces)。
5014
+ // 故此处只剩浮层一个节点;'page' / 'both' 档的「会话页」由 conversation.view 槽位承载。
5015
+ return nodes.length === 1 ? nodes[0] : h('div', { 'data-dam-panel-host': '' }, nodes)
4627
5016
  }
4628
5017
 
4629
5018
  // ───────────────────────── 调试中心(为提 issue 提供诊断) ─────────────────────────
@@ -4686,6 +5075,100 @@ window.__ModuleLoader__.load({
4686
5075
  var hb = data.heartbeat || {}
4687
5076
  var hbAge = hb.exists && hb.heartbeatAt ? Math.round((Date.now() - hb.heartbeatAt) / 1000) : -1
4688
5077
  var apiBad = probes && Object.keys(probes).some(function (k) { return probes[k].status === 404 || probes[k].error })
5078
+ // ★P0-2(2026-09-22):hub 三层记忆(episodes/facts/procedures)落盘 IO 健康度。
5079
+ // 宿主 debugInfo().associativeMemory.hubIo 早已产出(lib/index.js:6959 → _hubIoViewSnapshot),
5080
+ // 但前端零消费者 ⇒ save 失败被静默吞掉时,面板/日志/返回值三处都看不出「记忆到底写进磁盘了没有」。
5081
+ // 防御式取值:宿主未升级 / 字段缺失时整行不渲染,绝不因此报错。
5082
+ var am = data.associativeMemory && typeof data.associativeMemory === 'object' ? data.associativeMemory : {}
5083
+ var hubIo = am.hubIo && typeof am.hubIo === 'object' ? am.hubIo : null
5084
+ var hubIoWarn = false
5085
+ var hubIoText = ''
5086
+ if (hubIo) {
5087
+ var hubErrs = Number(hubIo.errors) || 0
5088
+ var hubLoads = Number(hubIo.loads) || 0
5089
+ var hubSaves = Number(hubIo.saves) || 0
5090
+ var hubClears = Number(hubIo.clears) || 0
5091
+ hubIoWarn = hubErrs > 0 || hubIo.verdict === 'io-error'
5092
+ if (hubErrs > 0) {
5093
+ hubIoText = (locale === 'zh' ? '⚠ 累计失败 ' + hubErrs + ' 次 · 最近: ' : '⚠ ' + hubErrs + ' failure(s) · latest: ') +
5094
+ (hubIo.lastError || t('unknown')) +
5095
+ (hubIo.lastErrorAt ? ' (' + new Date(hubIo.lastErrorAt).toLocaleTimeString() + ')' : '')
5096
+ } else if (hubLoads + hubSaves + hubClears > 0) {
5097
+ hubIoText = locale === 'zh'
5098
+ ? '✓ 正常(读 ' + hubLoads + ' · 写 ' + hubSaves + ' · 清 ' + hubClears + ',共 ' + (hubLoads + hubSaves + hubClears) + ' 次落盘操作无失败)'
5099
+ : '✓ healthy (load ' + hubLoads + ' · save ' + hubSaves + ' · clear ' + hubClears + ', ' + (hubLoads + hubSaves + hubClears) + ' ops, 0 failures)'
5100
+ } else {
5101
+ hubIoText = locale === 'zh' ? '尚无落盘动作(本次运行还没有写入/读取)' : 'no disk activity yet this run'
5102
+ }
5103
+ // 无失败时优先采用宿主给出的中文整句 summary(如「三层记忆已正常落盘(本轮无写入失败)」)——
5104
+ // 不在前端重拼一遍人话,避免两侧口径分叉。有失败时保留上面的详细行(含条数与人话原因 + 时间)。
5105
+ if (hubErrs === 0 && hubIo.summary) hubIoText = String(hubIo.summary)
5106
+ }
5107
+ var hubIoByFile = hubIo && hubIoWarn && hubIo.byFile && typeof hubIo.byFile === 'object'
5108
+ ? Object.keys(hubIo.byFile).map(function (k) {
5109
+ var rec = hubIo.byFile[k] || {}
5110
+ return k + ' × ' + (Number(rec.count) || 0)
5111
+ }).join(' ')
5112
+ : ''
5113
+ // ★P0-3(2026-09-22):事实保留上限淘汰台账。
5114
+ // getLastPrune()/retentionLimit()(fact-store.js:830/834)此前在宿主与前端**双侧**零调用方,
5115
+ // 「事实被淘汰了」无处可查。只展示聚合数(条数/上限/超额/最旧时间),不展示任何事实正文。
5116
+ var fp = am.factsPrune && typeof am.factsPrune === 'object' ? am.factsPrune : null
5117
+ var fpWarn = false
5118
+ var fpText = ''
5119
+ if (fp) {
5120
+ var fpPruned = Number(fp.pruned) || 0
5121
+ var fpLimit = fp.limit === undefined || fp.limit === null ? '—' : fp.limit
5122
+ fpWarn = fpPruned > 0
5123
+ if (fpPruned > 0) {
5124
+ fpText = locale === 'zh'
5125
+ ? '上次淘汰 ' + fpPruned + ' 条(上限 ' + fpLimit + (fp.over ? ',超出 ' + fp.over : '') + ')' +
5126
+ (fp.protected ? ' · 另有 ' + fp.protected + ' 条重要/未撤销条目受保护未删' : '') +
5127
+ (fp.at ? ' · ' + new Date(fp.at).toLocaleTimeString() : '')
5128
+ : 'last prune removed ' + fpPruned + ' (cap ' + fpLimit + (fp.over ? ', over ' + fp.over : '') + ')' +
5129
+ (fp.protected ? ' · ' + fp.protected + ' protected (important/unrevoked) kept' : '') +
5130
+ (fp.at ? ' · ' + new Date(fp.at).toLocaleTimeString() : '')
5131
+ } else {
5132
+ fpText = locale === 'zh'
5133
+ ? '✓ 本次运行未发生淘汰(上限 ' + fpLimit + ')'
5134
+ : '✓ no eviction yet this run (cap ' + fpLimit + ')'
5135
+ }
5136
+ }
5137
+ // ★P1-1 补接线(2026-09-22):Python 语义侧车看门狗 + stderr 尾部。
5138
+ // 宿主 debugInfo().associativeMemory.pythonBackend 早已产出(lib/index.js:6944 →
5139
+ // Object.assign({ enabled }, client.debugView())),前端此前零渲染 ⇒ 侧车被看门狗
5140
+ // 击杀/重生、以及 Python traceback 原文,用户一概看不到;现象只剩「语义唤回突然不好使了」,
5141
+ // 无法归因。★铁律:Python 档是可选进阶项,未启用**不影响** JS 端语义 —— 文案必须写明,
5142
+ // 否则用户会误判成「记忆坏了」。防御式取值:宿主未升级/字段缺失时整块不渲染,绝不报错。
5143
+ var pySide = am.pythonBackend && typeof am.pythonBackend === 'object' ? am.pythonBackend : null
5144
+ var pySideWarn = false
5145
+ var pySideState = ''
5146
+ var pySideWd = ''
5147
+ var pySideTail = ''
5148
+ var pySideHasTail = false
5149
+ var pySideTrunc = false
5150
+ if (pySide) {
5151
+ var pyKills = Number(pySide.watchdog && pySide.watchdog.kills) || 0
5152
+ var pyRespawns = Number(pySide.watchdog && pySide.watchdog.respawns) || 0
5153
+ var pyGen = Number(pySide.generation) || 0
5154
+ var pyReason = pySide.watchdog && pySide.watchdog.lastReason ? String(pySide.watchdog.lastReason) : ''
5155
+ var pyBytes = Number(pySide.stderrTailBytes) || 0
5156
+ pySideWarn = pyKills > 0
5157
+ pySideState = pySide.enabled !== true
5158
+ ? t('kvPyOff')
5159
+ : (pySide.started === true
5160
+ ? t('kvPyAlive') + (pyGen > 0 ? (locale === 'zh' ? '(第 ' + pyGen + ' 代)' : ' (gen ' + pyGen + ')') : '')
5161
+ : (locale === 'zh' ? '未运行(惰性启动:首次请求才拉起进程)' : 'not running (lazy: spawned on first request)'))
5162
+ pySideWd = pyKills === 0
5163
+ ? (locale === 'zh' ? '无(未触发)' : 'none')
5164
+ : (locale === 'zh'
5165
+ ? '击杀 ' + pyKills + ' 次 · 重生 ' + pyRespawns + ' 次' + (pyReason ? '(最近:' + pyReason + ')' : '')
5166
+ : 'kills ' + pyKills + ' · respawns ' + pyRespawns + (pyReason ? ' (latest: ' + pyReason + ')' : ''))
5167
+ if (pySide.lastStderrTail) { pySideTail = String(pySide.lastStderrTail); pySideHasTail = true }
5168
+ else if (pyBytes === 0) pySideTail = locale === 'zh' ? '确实没有输出(不是被截断)' : 'no output at all (not truncated)'
5169
+ else pySideTail = locale === 'zh' ? '有 stderr 输出但无可显示行(全为空白行)' : 'stderr output present but no printable line (blank only)'
5170
+ pySideTrunc = pySide.lastStderrTailTruncated === true
5171
+ }
4689
5172
  return h('div', null,
4690
5173
  h('div', { 'data-dam-row': '' },
4691
5174
  h('button', { 'data-dam-btn': '', onClick: refresh, disabled: busy }, busy ? t('dbgRefreshing') : t('dbgRefresh')),
@@ -4702,6 +5185,18 @@ window.__ModuleLoader__.load({
4702
5185
  kv(t('kvMem'), t('kvMemUser') + ' ' + (data.memoryFiles.user.exists ? fmtSize(data.memoryFiles.user.size) : t('kvMemMissing')) + ' · ' + t('kvMemNotes') + ' ' + (data.memoryFiles.notes.exists ? fmtSize(data.memoryFiles.notes.size) : t('kvMemMissing')) + ' · ' + t('kvMemLog') + ' ' + (data.memoryFiles.log.exists ? fmtSize(data.memoryFiles.log.size) : t('kvMemMissing')), !data.memoryFiles.log.exists),
4703
5186
  kv(t('kvWs'), currentWs() || t('kvWsUnknown'), false),
4704
5187
  kv(t('kvApi'), probes ? Object.keys(probes).map(function (k) { return k + '=' + (probes[k].status !== undefined ? probes[k].status : probes[k].error) }).join(' ') : '…', apiBad),
5188
+ hubIo ? kv(t('dbgHubIo'), hubIoText, hubIoWarn) : null,
5189
+ hubIoByFile ? h('div', { 'data-dam-hint': '', style: { marginLeft: '140px', wordBreak: 'break-all' } },
5190
+ (locale === 'zh' ? '按层拆分(失败次数):' : 'per-store failures: ') + hubIoByFile) : null,
5191
+ fp ? kv(t('dbgPrune'), fpText, fpWarn) : null,
5192
+ pySide ? h('div', { 'data-dam-row': '', style: { marginTop: '3px', flexDirection: 'column', alignItems: 'stretch' } },
5193
+ kv(t('kvPyTitle'), pySideState, false),
5194
+ kv(t('kvPyWatchdog'), pySideWd, pySideWarn),
5195
+ h('div', { 'data-dam-hint': '', style: { marginTop: '3px' } },
5196
+ t('kvPyStderr') + (pySideTrunc ? t('kvPyStderrTrunc') : '') + ':' + pySideTail,
5197
+ pySideHasTail
5198
+ ? h('pre', { style: { margin: '3px 0 0 0', padding: '6px 8px', maxHeight: '200px', overflow: 'auto', whiteSpace: 'pre-wrap', wordBreak: 'break-all', background: 'rgba(127,127,127,.10)', borderRadius: '6px', fontSize: 'calc(11px * var(--dam-scale))' } }, pySideTail)
5199
+ : null)) : null,
4705
5200
  h('div', { 'data-dam-row': '', style: { marginTop: '6px', alignItems: 'flex-start', flexDirection: 'column' } },
4706
5201
  h('div', { 'data-dam-row': '' },
4707
5202
  h('button', { 'data-dam-btn': '', title: 'prion-scan 式四类启发式,只报位置不含正文', onClick: runScan, disabled: scanBusy }, scanBusy ? (locale === 'zh' ? '扫描中…' : 'Scanning…') : (locale === 'zh' ? '扫描脏 token' : 'Scan dirty tokens')),
@@ -4878,7 +5373,9 @@ window.__ModuleLoader__.load({
4878
5373
  toggles: [
4879
5374
  { key: 'activationEmitMode', name: locale === 'zh' ? '记忆唤起(链中检测→插入)' : 'Memory recall (detect → inject)', sub: locale === 'zh' ? '开=canary 档(显式回忆注入,推荐)/ 关=shadow 档(只记录不注入)' : 'On = canary (inject on explicit recall, recommended) / Off = shadow (record only)', mode: true, rec: true, where: locale === 'zh' ? '自动记忆引擎' : 'Semantic engine' },
4880
5375
  { key: 'autoConsolidate', name: locale === 'zh' ? '记忆固化(自动沉淀)' : 'Memory consolidation', sub: locale === 'zh' ? '每轮对话结束自动把结论沉淀进每日日志与记忆店(推荐开)' : 'Consolidate conclusions into the log & stores each turn (recommended)', rec: true, where: locale === 'zh' ? '自动化' : 'Automation' },
4881
- { key: 'procedurePromotionEnabled', name: locale === 'zh' ? '技能固化与晋升' : 'Skill crystallization', sub: locale === 'zh' ? '重复流程固化为 checklist 自动附上;跨会话验证后晋升(「记忆中枢」页审批)' : 'Turn repeated flows into auto-attached checklists; promote after validation (Memory Hub tab)', where: locale === 'zh' ? '记忆中枢' : 'Memory Hub' },
5376
+ // ★2026-09-22 改键(设置页缺口审计 P0-2):旧键 procedurePromotionEnabled 因 DEFAULT_CONFIG
5377
+ // 恒供给布尔 procedureInjectEnabled 而**结构性不可达** ⇒ 该开关此前是摆设(会翻面、写盘成功、宿主读不到)。
5378
+ { key: 'procedureInjectEnabled', name: locale === 'zh' ? '技能固化与注入' : 'Skill crystallization', sub: locale === 'zh' ? '重复流程固化为 checklist 自动附上;跨会话验证后晋升(「记忆中枢」页审批)' : 'Turn repeated flows into auto-attached checklists; promote after validation (Memory Hub tab)', where: locale === 'zh' ? '记忆中枢' : 'Memory Hub' },
4882
5379
  // ★T10(2026-09-20):机械 procedure 切片开关。
4883
5380
  // 背景:`crossFeed()` 会把 episode 意图**机械截断**成技能名与步骤(无模型介入),
4884
5381
  // 产出的观察行「既不像技能名也不像步骤」——用户报「审批里看不懂」的来源。
@@ -5354,7 +5851,7 @@ window.__ModuleLoader__.load({
5354
5851
  var dbgOpenPair = useState(false)
5355
5852
  var dbgOpen = dbgOpenPair[0]
5356
5853
  var setDbgOpen = dbgOpenPair[1]
5357
- var settingsSectionPair = useState('appearance')
5854
+ var settingsSectionPair = useState('engine')
5358
5855
  var settingsSection = settingsSectionPair[0]
5359
5856
  var setSettingsSection = settingsSectionPair[1]
5360
5857
  var browseOpenPair = useState(false)
@@ -5635,12 +6132,15 @@ window.__ModuleLoader__.load({
5635
6132
  } catch (_) {}
5636
6133
  }
5637
6134
  var sectionLabels = {
5638
- semantic: locale === 'zh' ? '自动记忆引擎' : 'Semantic engine',
5639
- memoryHub: locale === 'zh' ? '记忆中枢' : 'Memory Hub',
5640
- appearance: locale === 'zh' ? '外观' : 'Appearance', storage: locale === 'zh' ? '存储' : 'Storage',
5641
- injection: locale === 'zh' ? '记忆窗口' : 'Memory window', automation: locale === 'zh' ? '自动化' : 'Automation',
5642
- context: locale === 'zh' ? '上下文管理' : 'Context management',
5643
- maintenance: locale === 'zh' ? '维护' : 'Maintenance'
6135
+ engine: locale === 'zh' ? '语义记忆总开关' : 'Memory engine',
6136
+ window: locale === 'zh' ? '记忆窗口(注入什么)' : 'Memory window',
6137
+ capacity: locale === 'zh' ? '记忆容量与归档' : 'Capacity & retention',
6138
+ skills: locale === 'zh' ? '自动沉淀成技能' : 'Skills & promotion',
6139
+ handoff: locale === 'zh' ? '长会话接续' : 'Handoff & continuation',
6140
+ auto: locale === 'zh' ? '自动化与免打扰' : 'Automation',
6141
+ store: locale === 'zh' ? '存储与外部记忆' : 'Storage',
6142
+ look: locale === 'zh' ? '外观与交互' : 'Appearance',
6143
+ about: locale === 'zh' ? '关于与诊断' : 'About',
5644
6144
  }
5645
6145
  function section(key, title, content) { return h('section', { id: 'dam-settings-' + key, 'data-dam-settings-group': '' }, h('h3', null, title), content) }
5646
6146
  function jumpToSection(key) {
@@ -5652,7 +6152,7 @@ window.__ModuleLoader__.load({
5652
6152
  return h('button', { key: key, 'data-dam-btn': '', 'data-active': settingsSection === key ? 'true' : undefined, onClick: function () { jumpToSection(key) } }, sectionLabels[key])
5653
6153
  })),
5654
6154
  h('div', { 'data-dam-settings-content': '' },
5655
- section('semantic', sectionLabels.semantic, [
6155
+ section('engine', sectionLabels.engine, [
5656
6156
  field(t('fAssocEngine'), h('input', { type: 'checkbox', checked: !!cfg.associativeMemoryEnabled, onChange: function (e) { set('associativeMemoryEnabled', e.target.checked) } }), t('fAssocEngineHint')),
5657
6157
  field(t('fAnchorIndex'), h('input', { type: 'checkbox', checked: !!cfg.memoryAnchorEnabled, onChange: function (e) { set('memoryAnchorEnabled', e.target.checked) } }), t('fAnchorIndexHint')),
5658
6158
  field(t('fJsCooldown'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 60, value: cfg.jsDecideCooldownRounds === undefined ? 1 : cfg.jsDecideCooldownRounds, onChange: function (e) { set('jsDecideCooldownRounds', (function () { var v = Number(e.target.value); return (Number.isFinite(v) && v >= 0) ? v : 1 })()) /* #19: 0=不冷却 是合法值,|| 1 会吃掉 0 */ } }), t('fJsCooldownHint')),
@@ -5687,6 +6187,109 @@ window.__ModuleLoader__.load({
5687
6187
  locale === 'zh' ? '⚠ 已选 JS 引擎,但本地模型未就绪——当前实际生效:词法兜底。下方安装引导卡可下载模型(约130MB),完成后自动启用。' : '⚠ JS engine selected but the local model is not ready — lexical fallback is active. Use the setup card below to download the model (~130MB); it engages automatically when done.') : null,
5688
6188
  (sem.loaded && cfg.semanticEngineMode === 'python' && sem.pythonInt8Present === false) ? h('div', { 'data-dam-error': '', style: { marginTop: '-4px', marginBottom: '8px' } },
5689
6189
  locale === 'zh' ? '⚠ 已选 Python 引擎,但 BGE-M3 int8 模型未就绪——当前实际生效:词法兜底。下方安装向导可引导安装;若本机无法安装(如架构不支持),请改用 JS 引擎或 auto。' : '⚠ Python engine selected but the BGE-M3 int8 model is not ready — lexical fallback is active. Use the wizard below; if this machine cannot install it, switch to the JS engine or auto.') : null,
6190
+ field(t('fReasoning'), h('input', { type: 'checkbox', checked: !!cfg.reasoningObserverEnabled, onChange: function (e) { set('reasoningObserverEnabled', e.target.checked) } }), t('fReasoningHint')),
6191
+ field(locale === 'zh' ? '唤起阈值(校准策略)' : 'Activation thresholds (calibrated)', h('div', null,
6192
+ h('span', null, 'tauHi 0.45 · tauLo 0.35 · deltaExp 0.03 · deltaPro 0.05' + (sem.loaded ? ((locale === 'zh' ? ' · 发射模式:' : ' · emit: ') + (sem.activationEmitMode || 'shadow')) : ''))),
6193
+ t('fTuningHint')),
6194
+ ]),
6195
+ section('window', sectionLabels.window, [
6196
+ field(t('fInject'), h('input', { type: 'checkbox', checked: !!cfg.injectEnabled, onChange: function (e) { set('injectEnabled', e.target.checked) } }), t('fInjectHint')),
6197
+ field(t('fBudget'), h('input', { 'data-dam-input': '', type: 'number', min: 400, value: cfg.injectBudgetChars, onChange: function (e) { set('injectBudgetChars', Number(e.target.value) || 2400) } }), t('fBudgetHint')),
6198
+ // 2026-09-22 补接线(设置页缺口审计 P0-3/P0-4/P0-6):目录层与规则分层此前只有后端实现。
6199
+ field(t('fTier0Catalog'), h('input', { type: 'checkbox', 'data-dam-key': 'tier0CatalogEnabled', checked: cfg.tier0CatalogEnabled !== false, onChange: function (e) { set('tier0CatalogEnabled', e.target.checked) } }), t('fTier0CatalogHint')),
6200
+ field(t('fTier0Max'), h('input', { 'data-dam-input': '', type: 'number', min: 100, value: cfg.tier0MaxTokens === undefined ? 400 : cfg.tier0MaxTokens, onChange: function (e) { set('tier0MaxTokens', Math.max(100, Number(e.target.value) || 400)) } }), t('fTier0MaxHint')),
6201
+ field(t('fRulesLayering'), h('input', { type: 'checkbox', 'data-dam-key': 'rulesLayeringMode', checked: String(cfg.rulesLayeringMode || 'self').toLowerCase() !== 'off', onChange: function (e) { set('rulesLayeringMode', e.target.checked ? 'self' : 'off') } }), t('fRulesLayeringHint')),
6202
+ field(t('fDays'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 14, value: cfg.recentDaysInjected, onChange: function (e) { set('recentDaysInjected', Number(e.target.value) || 1) } }), t('fDaysHint')),
6203
+ field(t('fExtBudget'), h('input', { 'data-dam-input': '', type: 'number', min: 200, value: cfg.externalInjectionChars === undefined ? 1400 : cfg.externalInjectionChars, onChange: function (e) { set('externalInjectionChars', Number(e.target.value) || 1400) } }), t('fExtBudgetHint')),
6204
+ field(t('fSnapGap'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 50, value: cfg.snapshotMinGapRounds === undefined ? 5 : cfg.snapshotMinGapRounds, onChange: function (e) { set('snapshotMinGapRounds', Number(e.target.value) || 5) } }), t('fSnapGapHint')),
6205
+ field(t('fReinjectOnCompact'), h('input', { type: 'checkbox', checked: cfg.snapshotReinjectOnCompact !== false, onChange: function (e) { set('snapshotReinjectOnCompact', e.target.checked) } }), t('fReinjectOnCompactHint')),
6206
+ field(t('fPromptCustom'), h('button', { 'data-dam-btn': '', onClick: function () { setPromptEditOpen(!promptEditOpen) } }, (promptEditOpen ? (locale === 'zh' ? '收起' : 'Collapse') : (locale === 'zh' ? '编辑 prompt 层' : 'Edit prompt layers'))), t('fPromptCustomHint')),
6207
+ promptEditOpen ? [
6208
+ h('div', { key: '__layers', style: { padding: '6px 0 2px', width: '100%' } },
6209
+ (Object.keys(DEFAULT_PROMPT_LAYERS_CLIENT)).map(function (k) {
6210
+ return h('div', { key: k, style: { marginBottom: '6px' } },
6211
+ h('div', { 'data-dam-hint': '', style: { fontWeight: 700, marginBottom: '2px' } }, k),
6212
+ h('textarea', { 'data-dam-input': '', rows: 2, style: { width: '100%', fontFamily: 'monospace', fontSize: 'calc(11px * var(--dam-scale))' }, value: (cfg.promptLayerOverrides || {})[k] || '', placeholder: DEFAULT_PROMPT_LAYERS_CLIENT[k] || '(默认文案)', onChange: function (e) { var ov = Object.assign({}, cfg.promptLayerOverrides || {}); if (e.target.value.trim() === '') delete ov[k]; else ov[k] = e.target.value; set('promptLayerOverrides', ov) } }))
6213
+ })),
6214
+ h('div', { key: '__reset', 'data-dam-row': '', style: { marginTop: '6px' } },
6215
+ h('button', { 'data-dam-btn': '', onClick: function () { set('promptLayerOverrides', {}) } }, locale === 'zh' ? '一键恢复默认' : 'Reset to defaults'))
6216
+ ] : null,
6217
+ ]),
6218
+ section('capacity', sectionLabels.capacity, [
6219
+ field(t('fChildObs'), h('input', { type: 'checkbox', checked: !!cfg.contextBridgeObserveChildSessions, onChange: function (e) { set('contextBridgeObserveChildSessions', e.target.checked) } }), t('fChildObsHint')),
6220
+ field(t('fEpisodicRet'), h('input', { 'data-dam-input': '', type: 'number', min: 16, max: 4096, value: cfg.episodicRetention === undefined ? 256 : cfg.episodicRetention, onChange: function (e) { set('episodicRetention', Math.max(16, Math.min(4096, Number(e.target.value) || 256))) } }), t('fEpisodicRetHint')),
6221
+ // 2026-09-22 补接线(P0-4):facts 淘汰上限(fact-store.js pruneIfNeeded)。
6222
+ field(t('fFactRetention'), h('input', { 'data-dam-input': '', type: 'number', min: 10, value: cfg.factRetentionMax === undefined ? 1000 : cfg.factRetentionMax, onChange: function (e) { set('factRetentionMax', Math.max(10, Number(e.target.value) || 1000)) } }), t('fFactRetentionHint')),
6223
+ field(t('fNoteCap'), h('input', { 'data-dam-input': '', type: 'number', min: 500, value: cfg.noteCapacityChars === undefined ? 24000 : cfg.noteCapacityChars, onChange: function (e) { set('noteCapacityChars', Number(e.target.value) || 24000) } }), t('fNoteCapHint')),
6224
+ field(t('fUserCap'), h('input', { 'data-dam-input': '', type: 'number', min: 500, value: cfg.userCapacityChars === undefined ? 24000 : cfg.userCapacityChars, onChange: function (e) { set('userCapacityChars', Number(e.target.value) || 24000) } }), t('fUserCapHint')),
6225
+ field(t('fConsolidateMin'), h('input', { 'data-dam-input': '', type: 'number', min: 80, value: cfg.autoConsolidateMinChars === undefined ? 240 : cfg.autoConsolidateMinChars, onChange: function (e) { set('autoConsolidateMinChars', Number(e.target.value) || 240) } }), t('fConsolidateMinHint')),
6226
+ field(t('fAutoConsolidate'), h('input', { type: 'checkbox', checked: cfg.autoConsolidate !== false, onChange: function (e) { set('autoConsolidate', e.target.checked) } }), t('fAutoConsolidateHint')),
6227
+ field(t('fConsolidate'), h('input', { 'data-dam-input': '', type: 'number', min: 5, value: cfg.autoConsolidateCooldownMinutes === undefined ? 30 : cfg.autoConsolidateCooldownMinutes, onChange: function (e) { set('autoConsolidateCooldownMinutes', Number(e.target.value) || 30) } }), t('fConsolidateHint')),
6228
+ field(t('fConsolidateMax'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 50, value: cfg.autoConsolidateDailyMax === undefined ? 8 : cfg.autoConsolidateDailyMax, onChange: function (e) { set('autoConsolidateDailyMax', Number(e.target.value) || 8) } }), t('fConsolidateMaxHint')),
6229
+ field(t('fMemFileIndex'), h('input', { type: 'checkbox', 'data-dam-key': 'memoryFileIndexEnabled', checked: cfg.memoryFileIndexEnabled === true, onChange: function (e) { set('memoryFileIndexEnabled', e.target.checked) } }), t('fMemFileIndexHint')),
6230
+ // 2026-09-14 解耦:交接白板与自动接续各自独立成项(旧实现只在白板页有接续按钮,且 enabled 还被
6231
+ // handoffEnabled 二次与运算)。两者出厂默认均为 false(功能仍在测试期),这里与白板页读写完全相同的配置键。
6232
+ ]),
6233
+ section('skills', sectionLabels.skills, [
6234
+ h('div', { 'data-dam-hint': '' }, t('secMemoryHubHint')),
6235
+ field(t('fMemoryHub'), h('input', { type: 'checkbox', checked: !!cfg.memoryHubEnabled, onChange: function (e) { set('memoryHubEnabled', e.target.checked) } }), t('fMemoryHubHint')),
6236
+ field(t('fEpisodicMin'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 16, value: cfg.episodicMinSegments === undefined ? 2 : cfg.episodicMinSegments, onChange: function (e) { set('episodicMinSegments', Math.max(1, Math.min(16, Number(e.target.value) || 2))) } }), t('fEpisodicMinHint')),
6237
+ field(t('fProcSessions'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 20, value: cfg.procedureMinSessions === undefined ? 3 : cfg.procedureMinSessions, onChange: function (e) { set('procedureMinSessions', Math.max(1, Math.min(20, Number(e.target.value) || 3))) } }), t('fProcSessionsHint')),
6238
+ field(t('fProcSuccess'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 20, value: cfg.procedureMinSuccess === undefined ? 2 : cfg.procedureMinSuccess, onChange: function (e) { set('procedureMinSuccess', Math.max(1, Math.min(20, Number(e.target.value) || 2))) } }), t('fProcSuccessHint')),
6239
+ field(t('fProcCorr'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 1, step: 0.05, value: cfg.procedureCorrectionCap === undefined ? 0.3 : cfg.procedureCorrectionCap, onChange: function (e) { set('procedureCorrectionCap', Math.max(0, Math.min(1, Number(e.target.value) || 0.3))) } }), t('fProcCorrHint')),
6240
+ field(t('fProcRisk'), h('input', { type: 'checkbox', checked: cfg.procedureHighRiskApproval !== false, onChange: function (e) { set('procedureHighRiskApproval', e.target.checked) } }), t('fProcRiskHint')),
6241
+ // 2026-09-22 补接线(设置页缺口审计 P0-1):procedureInjectEnabled 才是宿主真读的总闸
6242
+ // (procedure-switch.js 先判它);此前设置页只有旧键 ⇒ 用户改了也无效(灰控件)。
6243
+ field(t('fProcInject'), h('input', { type: 'checkbox', 'data-dam-key': 'procedureInjectEnabled', checked: cfg.procedureInjectEnabled !== false, onChange: function (e) { set('procedureInjectEnabled', e.target.checked) } }), t('fProcInjectHint')),
6244
+ field(t('fProcLevel'), h('select', { 'data-dam-select': '', value: cfg.procedureActiveLevel || 'checklist', onChange: function (e) { set('procedureActiveLevel', e.target.value) } },
6245
+ h('option', { value: 'checklist' }, locale === 'zh' ? 'checklist 完整步骤' : 'checklist (full steps)'),
6246
+ h('option', { value: 'excerpt' }, locale === 'zh' ? 'excerpt 摘要' : 'excerpt (summary)'),
6247
+ h('option', { value: 'hint' }, locale === 'zh' ? 'hint 仅提示' : 'hint (hint only)')), t('fProcLevelHint')),
6248
+ h('div', { 'data-dam-hint': '', style: { marginTop: '4px' } }, t('memoryHubViewHint')),
6249
+ ]),
6250
+ section('handoff', sectionLabels.handoff, [
6251
+ field(t('fHandoff'), h('input', { type: 'checkbox', 'data-dam-key': 'handoffEnabled', checked: cfg.handoffEnabled !== false, onChange: function (e) { set('handoffEnabled', e.target.checked) } }), t('fHandoffHint')),
6252
+ field(t('fAutoContinue'), h('input', { type: 'checkbox', 'data-dam-key': 'autoContinueEnabled', checked: cfg.autoContinueEnabled !== false, onChange: function (e) { set('autoContinueEnabled', e.target.checked) } }), t('fAutoContinueHint')),
6253
+ 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')),
6254
+ // 2026-09-22 补接线(设置页缺口审计 P0-5):criteriaGate 此前只能改文件才能退。
6255
+ field(t('fCriteriaGate'), h('input', { type: 'checkbox', 'data-dam-key': 'criteriaGate', checked: cfg.criteriaGate !== false, onChange: function (e) { set('criteriaGate', e.target.checked) } }), t('fCriteriaGateHint')),
6256
+ 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')),
6257
+ 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')),
6258
+ 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')),
6259
+ field(t('fWaterAdvisory'), h('input', { type: 'checkbox', checked: cfg.waterLevelAdvisory !== false, onChange: function (e) { set('waterLevelAdvisory', e.target.checked) } }), t('fWaterAdvisoryHint')),
6260
+ field(t('fWaterAuto'), h('input', { type: 'checkbox', checked: cfg.waterLevelAutoHandoff !== false, onChange: function (e) { set('waterLevelAutoHandoff', e.target.checked) } }), t('fWaterAutoHint')),
6261
+ field(locale === 'zh' ? '子代理痕迹回收' : 'Subagent trace recycle', h('input', { type: 'checkbox', checked: cfg.subagentGcEnabled !== false, onChange: function (e) { set('subagentGcEnabled', e.target.checked) } }),
6262
+ locale === 'zh' ? '本插件的一次性子代理(自动沉淀 / 时段总结 / 问候 / 蒸馏)每跑一次都会在 ~/.dsh/sessions 留下一个持久化会话;数量上千后会话列表会明显变慢。开启后任务一结束就把这些痕迹移入 ~/.dsh/subagent-gc-backup 备份(可回滚),不影响子代理结果。默认开。' : 'Every one-shot subagent this plugin spawns (auto-consolidation, scheduled summary, greeting, distillation) leaves a persisted session under ~/.dsh/sessions; hundreds of them slow the session list down. When on, each trace is moved into ~/.dsh/subagent-gc-backup right after the task ends (reversible), never affecting the subagent result. On by default.'),
6263
+ field(locale === 'zh' ? '兜底回收保留天数' : 'Fallback recycle keep days', h('input', { 'data-dam-input': '', type: 'number', min: 0, value: cfg.subagentGcKeepDays === undefined ? 3 : cfg.subagentGcKeepDays, onChange: function (e) { set('subagentGcKeepDays', Number(e.target.value) || 0) } }),
6264
+ locale === 'zh' ? '每天巡检一次,回收超过该天数仍残留的痕迹(比如任务异常中断没删掉的);0 = 不按时间,只靠任务结束即删。' : 'A daily sweep recycles traces older than this many days (e.g. leftovers from interrupted tasks); 0 = rely on the end-of-task recycle only.'),
6265
+ ]),
6266
+ section('auto', sectionLabels.auto, [
6267
+ // 欢迎向导:开关(首启自动播放)+ 立即重看按钮(闭包内直调 openDialog——同一作用域,点击立即弹;
6268
+ // 不走 window 全局入口,避免多实例时序导致"点了没反应要刷新")+ 查看更新日志(走 update 弹窗,
6269
+ // 带 Logo 开场动画;versions 取 CHANGELOG 最新一条)
6270
+ field(t('fWelcomeTour'), h('div', { 'data-dam-row': '' },
6271
+ h('input', { type: 'checkbox', checked: cfg.welcomeTourEnabled !== false, onChange: function (e) { set('welcomeTourEnabled', e.target.checked) } }),
6272
+ h('button', { 'data-dam-btn': '', onClick: function () { try { openDialog({ kind: 'welcomeTour' }) } catch (eTour) {} } }, t('tourReplay')),
6273
+ h('button', { 'data-dam-btn': '', onClick: function () {
6274
+ try {
6275
+ var keys = Object.keys(CHANGELOG)
6276
+ if (!keys.length) return
6277
+ var latest = keys.sort(cmpVersion)[keys.length - 1]
6278
+ openDialog({ kind: 'update', versions: [{ version: latest, items: CHANGELOG[latest] }], currentVersion: latest })
6279
+ } catch (eLog) {}
6280
+ } }, locale === 'zh' ? '查看更新日志' : 'View changelog')), t('fWelcomeTourHint')),
6281
+ field(t('fAutoPopup'), h('input', { type: 'checkbox', checked: cfg.autoPopupEnabled !== false, onChange: function (e) { set('autoPopupEnabled', e.target.checked) } }), t('fAutoPopupHint')),
6282
+ field(t('fUnattended'), h('input', { type: 'checkbox', checked: !!cfg.unattendedMode, onChange: function (e) { set('unattendedMode', e.target.checked) } }), t('fUnattendedHint')),
6283
+ field(t('fUnattendedAuto'), h('input', { type: 'checkbox', checked: !!cfg.unattendedAuto, onChange: function (e) { set('unattendedAuto', e.target.checked) } }), t('fUnattendedAutoHint')),
6284
+ field(t('fAway'), h('input', { 'data-dam-input': '', type: 'number', min: 0, value: (Number(cfg.awayMinutes) === 0 ? 0 : (cfg.awayMinutes || 60)), onChange: function (e) { var v = Number(e.target.value); set('awayMinutes', Number.isFinite(v) && v >= 0 ? v : 60) } }), t('fAwayHint')),
6285
+ field(t('fAutoSum'), h('input', { 'data-dam-input': '', value: (cfg.autoSummaryTimes || []).join(','), onChange: function (e) { set('autoSummaryTimes', String(e.target.value || '').split(',').map(function (s) { return s.trim() }).filter(Boolean)) } }), t('fAutoSumHint')),
6286
+ field(t('fConsSchedule'), h('input', { type: 'checkbox', checked: cfg.consolidateScheduleEnabled !== false, onChange: function (e) { set('consolidateScheduleEnabled', e.target.checked) } }), t('fConsScheduleHint')),
6287
+ field(t('fConsScheduleTime'), h('input', { 'data-dam-input': '', placeholder: '09:30', value: cfg.consolidateScheduleTime === undefined ? '09:30' : cfg.consolidateScheduleTime, onChange: function (e) { set('consolidateScheduleTime', String(e.target.value || '').trim()) } }), t('fConsScheduleTimeHint')),
6288
+ field(t('fConsScheduleDays'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 60, value: cfg.consolidateScheduleDays === undefined ? 7 : cfg.consolidateScheduleDays, onChange: function (e) { set('consolidateScheduleDays', Number(e.target.value) || 7) } }), t('fConsScheduleDaysHint')),
6289
+ field(t('fMaintSchedule'), h('input', { type: 'checkbox', checked: cfg.maintainScheduleEnabled !== false, onChange: function (e) { set('maintainScheduleEnabled', e.target.checked) } }), t('fMaintScheduleHint')),
6290
+ field(t('fMaintScheduleTime'), h('input', { 'data-dam-input': '', placeholder: '10:00', value: cfg.maintainScheduleTime === undefined ? '10:00' : cfg.maintainScheduleTime, onChange: function (e) { set('maintainScheduleTime', String(e.target.value || '').trim()) } }), t('fMaintScheduleTimeHint')),
6291
+ ]),
6292
+ section('store', sectionLabels.store, [
5690
6293
  // M-CM6-B v3·环境检测面板:⟳ 按钮触发,自动检索环境(快检+深扫+热接入),联动安装助手;美学对齐安装向导卡
5691
6294
  detOpen ? (function () {
5692
6295
  var panelStyle = { border: '1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.25)) 60%, transparent)', borderRadius: '10px', padding: '12px', marginBottom: '10px', background: 'color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06)) 55%, transparent)', backdropFilter: 'blur(14px) saturate(1.3)', WebkitBackdropFilter: 'blur(14px) saturate(1.3)' }
@@ -5794,175 +6397,102 @@ window.__ModuleLoader__.load({
5794
6397
  h('option', { value: 'auto' }, t('mAuto')),
5795
6398
  h('option', { value: 'cn' }, t('mCn')),
5796
6399
  h('option', { value: 'intl' }, t('mIntl'))) : null,
5797
- ready ? h('button', { 'data-dam-btn': '', onClick: function () { setGuide(''); var n2 = Object.assign({}, cfg); n2.semanticEngineMode = guide; if (guide === 'js') { n2.activationSource = 'js'; n2.contextSinkMode = 'null' } else if (guide === 'python') { n2.activationSource = 'python'; n2.contextSinkMode = 'python'; try { var gp = localStorage.getItem('dsh-auto-memory.pyGpu'); if (gp === '1') n2.pythonGpu = true } catch (eL) {} } setCfg(n2); setDirty(true) } }, locale === 'zh' ? '启用并继续' : 'Enable & continue') : null,
6400
+ // ★2026-09-22 删死键(设置页缺口审计 P0-7):pythonGpu 不在 DEFAULT_CONFIG 白名单,
6401
+ // /config 会静默丢弃、宿主 lib/ 零读取 ⇒ 写入无任何效果,故移除该段(行为零变化)。
6402
+ ready ? h('button', { 'data-dam-btn': '', onClick: function () { setGuide(''); var n2 = Object.assign({}, cfg); n2.semanticEngineMode = guide; if (guide === 'js') { n2.activationSource = 'js'; n2.contextSinkMode = 'null' } else if (guide === 'python') { n2.activationSource = 'python'; n2.contextSinkMode = 'python' } setCfg(n2); setDirty(true) } }, locale === 'zh' ? '启用并继续' : 'Enable & continue') : null,
5798
6403
  !ready && isJs && !dlActive ? h('button', { 'data-dam-btn': '', onClick: startDl }, (dl.phase === 'error' || dl.phase === 'cancelled') ? t('semDlRetry') : t('semDlStart')) : null,
5799
6404
  !ready && isJs && dlActive ? h('button', { 'data-dam-btn': '', onClick: cancelDl }, t('semDlCancel')) : null,
5800
6405
  h('button', { 'data-dam-btn': '', onClick: function () { setGuide('') } }, t('gotIt'))))
5801
6406
  })()
5802
6407
  : null,
5803
- field(t('fReasoning'), h('input', { type: 'checkbox', checked: !!cfg.reasoningObserverEnabled, onChange: function (e) { set('reasoningObserverEnabled', e.target.checked) } }), t('fReasoningHint')),
5804
- field(t('fChildObs'), h('input', { type: 'checkbox', checked: !!cfg.contextBridgeObserveChildSessions, onChange: function (e) { set('contextBridgeObserveChildSessions', e.target.checked) } }), t('fChildObsHint')),
5805
- field(locale === 'zh' ? '唤起阈值(校准策略)' : 'Activation thresholds (calibrated)', h('div', null,
5806
- h('span', null, 'tauHi 0.45 · tauLo 0.35 · deltaExp 0.03 · deltaPro 0.05' + (sem.loaded ? ((locale === 'zh' ? ' · 发射模式:' : ' · emit: ') + (sem.activationEmitMode || 'shadow')) : ''))),
5807
- t('fTuningHint')),
5808
- ]),
5809
- section('memoryHub', sectionLabels.memoryHub, [
5810
- h('div', { 'data-dam-hint': '' }, t('secMemoryHubHint')),
5811
- field(t('fMemoryHub'), h('input', { type: 'checkbox', checked: !!cfg.memoryHubEnabled, onChange: function (e) { set('memoryHubEnabled', e.target.checked) } }), t('fMemoryHubHint')),
5812
- field(t('fEpisodicMin'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 16, value: cfg.episodicMinSegments === undefined ? 2 : cfg.episodicMinSegments, onChange: function (e) { set('episodicMinSegments', Math.max(1, Math.min(16, Number(e.target.value) || 2))) } }), t('fEpisodicMinHint')),
5813
- field(t('fEpisodicRet'), h('input', { 'data-dam-input': '', type: 'number', min: 16, max: 4096, value: cfg.episodicRetention === undefined ? 256 : cfg.episodicRetention, onChange: function (e) { set('episodicRetention', Math.max(16, Math.min(4096, Number(e.target.value) || 256))) } }), t('fEpisodicRetHint')),
5814
- field(t('fProcSessions'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 20, value: cfg.procedureMinSessions === undefined ? 3 : cfg.procedureMinSessions, onChange: function (e) { set('procedureMinSessions', Math.max(1, Math.min(20, Number(e.target.value) || 3))) } }), t('fProcSessionsHint')),
5815
- field(t('fProcSuccess'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 20, value: cfg.procedureMinSuccess === undefined ? 2 : cfg.procedureMinSuccess, onChange: function (e) { set('procedureMinSuccess', Math.max(1, Math.min(20, Number(e.target.value) || 2))) } }), t('fProcSuccessHint')),
5816
- field(t('fProcCorr'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 1, step: 0.05, value: cfg.procedureCorrectionCap === undefined ? 0.3 : cfg.procedureCorrectionCap, onChange: function (e) { set('procedureCorrectionCap', Math.max(0, Math.min(1, Number(e.target.value) || 0.3))) } }), t('fProcCorrHint')),
5817
- field(t('fProcRisk'), h('input', { type: 'checkbox', checked: cfg.procedureHighRiskApproval !== false, onChange: function (e) { set('procedureHighRiskApproval', e.target.checked) } }), t('fProcRiskHint')),
5818
- field(t('fProcLevel'), h('select', { 'data-dam-select': '', value: cfg.procedureActiveLevel || 'checklist', onChange: function (e) { set('procedureActiveLevel', e.target.value) } },
5819
- h('option', { value: 'checklist' }, locale === 'zh' ? 'checklist 完整步骤' : 'checklist (full steps)'),
5820
- h('option', { value: 'excerpt' }, locale === 'zh' ? 'excerpt 摘要' : 'excerpt (summary)'),
5821
- h('option', { value: 'hint' }, locale === 'zh' ? 'hint 仅提示' : 'hint (hint only)')), t('fProcLevelHint')),
5822
- h('div', { 'data-dam-hint': '', style: { marginTop: '4px' } }, t('memoryHubViewHint')),
6408
+ field(t('fUserDir'), h('input', { 'data-dam-input': '', value: cfg.userMemoryDir, onChange: function (e) { set('userMemoryDir', e.target.value) } }), t('fUserDirHint')),
6409
+ field(t('fProjectDir'), h('input', { 'data-dam-input': '', value: cfg.projectMemoryDir, onChange: function (e) { set('projectMemoryDir', e.target.value) } }), t('fProjectDirHint')),
6410
+ field(t('fMemoryRoot'), h('div', { 'data-dam-row': '', style: { flex: 1 } },
6411
+ h('input', { 'data-dam-input': '', style: { flex: 1 }, value: cfg.memoryRoot || '', onChange: function (e) { set('memoryRoot', e.target.value) } }),
6412
+ h('button', { 'data-dam-btn': '', onClick: function () { openBrowser() } }, t('fBrowse'))), t('fMemoryRootHint')),
6413
+ browseOpen ? h('div', { style: { border: '1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.25)) 60%, transparent)', borderRadius: '8px', padding: '8px', marginBottom: '8px', background: 'color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06)) 40%, transparent)' } },
6414
+ h('div', { 'data-dam-row': '' },
6415
+ h('b', { style: { fontSize: 'calc(12px * var(--dam-scale))', wordBreak: 'break-all', flex: 1 } }, browsePath || '…'),
6416
+ h('button', { 'data-dam-btn': '', onClick: function () { browseTo(browseParent) }, disabled: browseParent === browsePath }, t('fUp'))),
6417
+ h('div', { style: { maxHeight: '160px', overflow: 'auto', marginTop: '4px' } },
6418
+ (browseDirs || []).map(function (d) {
6419
+ return h('button', { key: d.path, 'data-dam-btn': '', style: { display: 'block', width: '100%', textAlign: 'left', padding: '3px 6px' }, onClick: function () { browseTo(d.path) } }, '📁 ' + d.name)
6420
+ }).concat(browseDirs && !browseDirs.length ? [h('div', { key: 'e', 'data-dam-hint': '' }, t('empty'))] : [])),
6421
+ h('div', { 'data-dam-row': '', style: { marginTop: '6px' } },
6422
+ h('button', { 'data-dam-btn': '', onClick: function () { set('memoryRoot', browsePath); setBrowseOpen(false) } }, t('fSelectDir')),
6423
+ h('button', { 'data-dam-btn': '', onClick: function () { setBrowseOpen(false) } }, t('close'))))
6424
+ : null,
6425
+ field(t('fDayBoundary'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 1439, value: (cfg.dayBoundaryMinutes === undefined ? 450 : cfg.dayBoundaryMinutes), onChange: function (e) { set('dayBoundaryMinutes', parseInt(e.target.value || '0', 10)) } }), t('fDayBoundaryHint')),
6426
+ field(t('fReflect'), h('input', { type: 'checkbox', checked: !!cfg.reflectEnabled, onChange: function (e) { set('reflectEnabled', e.target.checked) } }), t('fReflectHint')),
6427
+ field(t('fStyle'), h('select', { 'data-dam-select': '', value: cfg.reflectStyle, onChange: function (e) { set('reflectStyle', e.target.value) } },
6428
+ STYLE_IDS.map(function (id) { return h('option', { key: id, value: id }, t('style' + id.charAt(0).toUpperCase() + id.slice(1))) })), t('fStyleHint')),
6429
+ // 2026-09-22 补接线(设置页缺口审计 P1/P2):扫描上限与索引快照前端零命中。
6430
+ field(t('fWsDiscover'), h('input', { 'data-dam-input': '', type: 'number', min: 5, max: 2000, value: cfg.workspaceDiscoverMax === undefined ? 200 : cfg.workspaceDiscoverMax, onChange: function (e) { set('workspaceDiscoverMax', Math.max(5, Math.min(2000, Number(e.target.value) || 200))) } }), t('fWsDiscoverHint')),
5823
6431
  ]),
5824
- section('appearance', sectionLabels.appearance, [
6432
+ section('look', sectionLabels.look, [
5825
6433
  h('div', { 'data-dam-hint': '' }, t('settingsHeader')),
5826
- // 欢迎向导:开关(首启自动播放)+ 立即重看按钮(闭包内直调 openDialog——同一作用域,点击立即弹;
5827
- // 不走 window 全局入口,避免多实例时序导致"点了没反应要刷新")+ 查看更新日志(走 update 弹窗,
5828
- // 带 Logo 开场动画;versions 取 CHANGELOG 最新一条)
5829
- field(t('fWelcomeTour'), h('div', { 'data-dam-row': '' },
5830
- h('input', { type: 'checkbox', checked: cfg.welcomeTourEnabled !== false, onChange: function (e) { set('welcomeTourEnabled', e.target.checked) } }),
5831
- h('button', { 'data-dam-btn': '', onClick: function () { try { openDialog({ kind: 'welcomeTour' }) } catch (eTour) {} } }, t('tourReplay')),
5832
- h('button', { 'data-dam-btn': '', onClick: function () {
5833
- try {
5834
- var keys = Object.keys(CHANGELOG)
5835
- if (!keys.length) return
5836
- var latest = keys.sort(cmpVersion)[keys.length - 1]
5837
- openDialog({ kind: 'update', versions: [{ version: latest, items: CHANGELOG[latest] }], currentVersion: latest })
5838
- } catch (eLog) {}
5839
- } }, locale === 'zh' ? '查看更新日志' : 'View changelog')), t('fWelcomeTourHint')),
5840
6434
  field(t('fLocale'), h('select', { 'data-dam-select': '', value: cfg.locale || 'system', onChange: function (e) { set('locale', e.target.value) } },
5841
6435
  LOCALE_IDS_LIST.map(function (id) { return h('option', { key: id, value: id }, id === 'system' ? t('followSystem') : t(id)) })), t('fLocaleHint')),
5842
6436
  field(t('fFontSize'), h('select', { 'data-dam-select': '', value: fontScale, onChange: function (e) { fontScale = e.target.value; try { localStorage.setItem('dsh-auto-memory.fontScale.v2', fontScale) } catch (ee) {}; try { var pp = document.querySelector('[data-dam-panel]'); if (pp) pp.style.setProperty('--dam-scale', FONT_SCALE_VALUES[fontScale] || '1') } catch (ee2) {}; emit() } },
5843
6437
  Object.keys(FONT_SCALES).map(function (k) { return h('option', { key: k, value: k }, t('fs' + k.charAt(0).toUpperCase() + k.slice(1))) })), t('fFontSizeHint')),
6438
+ // 面板位置(2026-09-21):左下角 / 顶部 / 两者共存。改动立即生效并同步两处形态(用户硬规则:即时回显)。
6439
+ field(t('fPanelPos'), h('select', { 'data-dam-select': '', value: controller.panelPos(), onChange: function (e) { controller.setPanelPos(e.target.value) } },
6440
+ h('option', { value: 'bottom-left' }, t('posBottomLeft')), h('option', { value: 'page' }, t('posPage')), h('option', { value: 'both' }, t('posBoth'))), t('fPanelPosHint')),
5844
6441
  field(locale === 'zh' ? '强调色' : 'Accent color', h('select', { 'data-dam-select': '', value: accentTheme, onChange: function (e) { setAccent(e.target.value) } },
5845
6442
  h('option', { value: 'deepseek' }, locale === 'zh' ? 'DeepSeek 蓝' : 'DeepSeek blue'), h('option', { value: 'graphite' }, locale === 'zh' ? '石墨灰' : 'Graphite'), h('option', { value: 'violet' }, locale === 'zh' ? '雾紫' : 'Violet')), locale === 'zh' ? '默认使用 DeepSeek 蓝;日历与状态颜色保持语义色。' : 'DeepSeek blue by default; calendar and status colors stay semantic.'),
5846
6443
  field(locale === 'zh' ? '关系图密度' : 'Graph density', h('select', { 'data-dam-select': '', value: graphDensity, onChange: function (e) { setDensity(e.target.value) } },
5847
- h('option', { value: 'relaxed' }, locale === 'zh' ? '舒展' : 'Relaxed'), h('option', { value: 'compact' }, locale === 'zh' ? '紧凑' : 'Compact')), locale === 'zh' ? '影响工作区关系图的节点间距和显示数量。' : 'Controls node spacing and detail in the workspace graph.')
5848
- ]),
5849
- section('storage', sectionLabels.storage, [field(t('fUserDir'), h('input', { 'data-dam-input': '', value: cfg.userMemoryDir, onChange: function (e) { set('userMemoryDir', e.target.value) } }), t('fUserDirHint')),
5850
- field(t('fProjectDir'), h('input', { 'data-dam-input': '', value: cfg.projectMemoryDir, onChange: function (e) { set('projectMemoryDir', e.target.value) } }), t('fProjectDirHint')),
5851
- field(t('fMemoryRoot'), h('div', { 'data-dam-row': '', style: { flex: 1 } },
5852
- h('input', { 'data-dam-input': '', style: { flex: 1 }, value: cfg.memoryRoot || '', onChange: function (e) { set('memoryRoot', e.target.value) } }),
5853
- h('button', { 'data-dam-btn': '', onClick: function () { openBrowser() } }, t('fBrowse'))), t('fMemoryRootHint')),
5854
- browseOpen ? h('div', { style: { border: '1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.25)) 60%, transparent)', borderRadius: '8px', padding: '8px', marginBottom: '8px', background: 'color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06)) 40%, transparent)' } },
5855
- h('div', { 'data-dam-row': '' },
5856
- h('b', { style: { fontSize: 'calc(12px * var(--dam-scale))', wordBreak: 'break-all', flex: 1 } }, browsePath || '…'),
5857
- h('button', { 'data-dam-btn': '', onClick: function () { browseTo(browseParent) }, disabled: browseParent === browsePath }, t('fUp'))),
5858
- h('div', { style: { maxHeight: '160px', overflow: 'auto', marginTop: '4px' } },
5859
- (browseDirs || []).map(function (d) {
5860
- return h('button', { key: d.path, 'data-dam-btn': '', style: { display: 'block', width: '100%', textAlign: 'left', padding: '3px 6px' }, onClick: function () { browseTo(d.path) } }, '📁 ' + d.name)
5861
- }).concat(browseDirs && !browseDirs.length ? [h('div', { key: 'e', 'data-dam-hint': '' }, t('empty'))] : [])),
5862
- h('div', { 'data-dam-row': '', style: { marginTop: '6px' } },
5863
- h('button', { 'data-dam-btn': '', onClick: function () { set('memoryRoot', browsePath); setBrowseOpen(false) } }, t('fSelectDir')),
5864
- h('button', { 'data-dam-btn': '', onClick: function () { setBrowseOpen(false) } }, t('close'))))
5865
- : null]),
5866
- section('injection', sectionLabels.injection, [field(t('fInject'), h('input', { type: 'checkbox', checked: !!cfg.injectEnabled, onChange: function (e) { set('injectEnabled', e.target.checked) } }), t('fInjectHint')),
5867
- field(t('fBudget'), h('input', { 'data-dam-input': '', type: 'number', min: 400, value: cfg.injectBudgetChars, onChange: function (e) { set('injectBudgetChars', Number(e.target.value) || 2400) } }), t('fBudgetHint')),
5868
- // 群反馈第 4 条:排除来源(每行一条)。textarea 便于粘贴多行路径;
5869
- // 空行/纯空白在写回时被过滤,避免"看起来配了其实没配"。
5870
- field(t('fExclude'), h('textarea', {
5871
- 'data-dam-input': '', rows: 3,
5872
- style: { width: '100%', resize: 'vertical', fontFamily: 'inherit' },
5873
- value: (Array.isArray(cfg.injectExcludeSources) ? cfg.injectExcludeSources : []).join('\n'),
5874
- placeholder: 'mem_0123456789abcdef0123456789abcdef\nD:\\ws\\.dsh-memory\\archive\\\nlog',
5875
- onChange: function (e) {
5876
- var lines = String(e.target.value || '').split('\n').map(function (x) { return x.trim() }).filter(Boolean)
5877
- set('injectExcludeSources', lines)
5878
- },
5879
- }), t('fExcludeHint')),
5880
- field(t('fNoteCap'), h('input', { 'data-dam-input': '', type: 'number', min: 500, value: cfg.noteCapacityChars === undefined ? 24000 : cfg.noteCapacityChars, onChange: function (e) { set('noteCapacityChars', Number(e.target.value) || 24000) } }), t('fNoteCapHint')),
5881
- field(t('fUserCap'), h('input', { 'data-dam-input': '', type: 'number', min: 500, value: cfg.userCapacityChars === undefined ? 24000 : cfg.userCapacityChars, onChange: function (e) { set('userCapacityChars', Number(e.target.value) || 24000) } }), t('fUserCapHint')),
5882
- field(t('fDays'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 14, value: cfg.recentDaysInjected, onChange: function (e) { set('recentDaysInjected', Number(e.target.value) || 1) } }), t('fDaysHint')),
5883
- field(t('fExtBudget'), h('input', { 'data-dam-input': '', type: 'number', min: 200, value: cfg.externalInjectionChars === undefined ? 1400 : cfg.externalInjectionChars, onChange: function (e) { set('externalInjectionChars', Number(e.target.value) || 1400) } }), t('fExtBudgetHint')),
5884
- field(t('fSnapGap'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 50, value: cfg.snapshotMinGapRounds === undefined ? 5 : cfg.snapshotMinGapRounds, onChange: function (e) { set('snapshotMinGapRounds', Number(e.target.value) || 5) } }), t('fSnapGapHint')),
5885
- field(t('fReinjectOnCompact'), h('input', { type: 'checkbox', checked: cfg.snapshotReinjectOnCompact !== false, onChange: function (e) { set('snapshotReinjectOnCompact', e.target.checked) } }), t('fReinjectOnCompactHint')),
5886
- field(t('fPromptCustom'), h('button', { 'data-dam-btn': '', onClick: function () { setPromptEditOpen(!promptEditOpen) } }, (promptEditOpen ? (locale === 'zh' ? '收起' : 'Collapse') : (locale === 'zh' ? '编辑 prompt 层' : 'Edit prompt layers'))), t('fPromptCustomHint')),
5887
- promptEditOpen ? [
5888
- h('div', { key: '__layers', style: { padding: '6px 0 2px', width: '100%' } },
5889
- (Object.keys(DEFAULT_PROMPT_LAYERS_CLIENT)).map(function (k) {
5890
- return h('div', { key: k, style: { marginBottom: '6px' } },
5891
- h('div', { 'data-dam-hint': '', style: { fontWeight: 700, marginBottom: '2px' } }, k),
5892
- h('textarea', { 'data-dam-input': '', rows: 2, style: { width: '100%', fontFamily: 'monospace', fontSize: 'calc(11px * var(--dam-scale))' }, value: (cfg.promptLayerOverrides || {})[k] || '', placeholder: DEFAULT_PROMPT_LAYERS_CLIENT[k] || '(默认文案)', onChange: function (e) { var ov = Object.assign({}, cfg.promptLayerOverrides || {}); if (e.target.value.trim() === '') delete ov[k]; else ov[k] = e.target.value; set('promptLayerOverrides', ov) } }))
5893
- })),
5894
- h('div', { key: '__reset', 'data-dam-row': '', style: { marginTop: '6px' } },
5895
- h('button', { 'data-dam-btn': '', onClick: function () { set('promptLayerOverrides', {}) } }, locale === 'zh' ? '一键恢复默认' : 'Reset to defaults'))
5896
- ] : null]),
5897
- section('automation', sectionLabels.automation, [field(t('fConsolidateMin'), h('input', { 'data-dam-input': '', type: 'number', min: 80, value: cfg.autoConsolidateMinChars === undefined ? 240 : cfg.autoConsolidateMinChars, onChange: function (e) { set('autoConsolidateMinChars', Number(e.target.value) || 240) } }), t('fConsolidateMinHint')),
5898
- field(t('fAutoConsolidate'), h('input', { type: 'checkbox', checked: cfg.autoConsolidate !== false, onChange: function (e) { set('autoConsolidate', e.target.checked) } }), t('fAutoConsolidateHint')),
5899
- field(t('fConsolidate'), h('input', { 'data-dam-input': '', type: 'number', min: 5, value: cfg.autoConsolidateCooldownMinutes === undefined ? 30 : cfg.autoConsolidateCooldownMinutes, onChange: function (e) { set('autoConsolidateCooldownMinutes', Number(e.target.value) || 30) } }), t('fConsolidateHint')),
5900
- field(t('fConsolidateMax'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 50, value: cfg.autoConsolidateDailyMax === undefined ? 8 : cfg.autoConsolidateDailyMax, onChange: function (e) { set('autoConsolidateDailyMax', Number(e.target.value) || 8) } }), t('fConsolidateMaxHint')),
5901
- field(t('fAutoPopup'), h('input', { type: 'checkbox', checked: cfg.autoPopupEnabled !== false, onChange: function (e) { set('autoPopupEnabled', e.target.checked) } }), t('fAutoPopupHint')),
5902
- field(t('fUnattended'), h('input', { type: 'checkbox', checked: !!cfg.unattendedMode, onChange: function (e) { set('unattendedMode', e.target.checked) } }), t('fUnattendedHint')),
5903
- field(t('fUnattendedAuto'), h('input', { type: 'checkbox', checked: !!cfg.unattendedAuto, onChange: function (e) { set('unattendedAuto', e.target.checked) } }), t('fUnattendedAutoHint')),
5904
- field(t('fAway'), h('input', { 'data-dam-input': '', type: 'number', min: 0, value: (Number(cfg.awayMinutes) === 0 ? 0 : (cfg.awayMinutes || 60)), onChange: function (e) { var v = Number(e.target.value); set('awayMinutes', Number.isFinite(v) && v >= 0 ? v : 60) } }), t('fAwayHint')),
5905
- field(t('fAutoSum'), h('input', { 'data-dam-input': '', value: (cfg.autoSummaryTimes || []).join(','), onChange: function (e) { set('autoSummaryTimes', String(e.target.value || '').split(',').map(function (s) { return s.trim() }).filter(Boolean)) } }), t('fAutoSumHint')),
5906
- field(t('fDayBoundary'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 1439, value: (cfg.dayBoundaryMinutes === undefined ? 450 : cfg.dayBoundaryMinutes), onChange: function (e) { set('dayBoundaryMinutes', parseInt(e.target.value || '0', 10)) } }), t('fDayBoundaryHint')),
5907
- field(t('fReflect'), h('input', { type: 'checkbox', checked: !!cfg.reflectEnabled, onChange: function (e) { set('reflectEnabled', e.target.checked) } }), t('fReflectHint')),
5908
- field(t('fConsSchedule'), h('input', { type: 'checkbox', checked: cfg.consolidateScheduleEnabled !== false, onChange: function (e) { set('consolidateScheduleEnabled', e.target.checked) } }), t('fConsScheduleHint')),
5909
- field(t('fConsScheduleTime'), h('input', { 'data-dam-input': '', placeholder: '09:30', value: cfg.consolidateScheduleTime === undefined ? '09:30' : cfg.consolidateScheduleTime, onChange: function (e) { set('consolidateScheduleTime', String(e.target.value || '').trim()) } }), t('fConsScheduleTimeHint')),
5910
- field(t('fConsScheduleDays'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 60, value: cfg.consolidateScheduleDays === undefined ? 7 : cfg.consolidateScheduleDays, onChange: function (e) { set('consolidateScheduleDays', Number(e.target.value) || 7) } }), t('fConsScheduleDaysHint')),
5911
- field(t('fMaintSchedule'), h('input', { type: 'checkbox', checked: cfg.maintainScheduleEnabled !== false, onChange: function (e) { set('maintainScheduleEnabled', e.target.checked) } }), t('fMaintScheduleHint')),
5912
- field(t('fMaintScheduleTime'), h('input', { 'data-dam-input': '', placeholder: '10:00', value: cfg.maintainScheduleTime === undefined ? '10:00' : cfg.maintainScheduleTime, onChange: function (e) { set('maintainScheduleTime', String(e.target.value || '').trim()) } }), t('fMaintScheduleTimeHint')),
5913
- field(t('fStyle'), h('select', { 'data-dam-select': '', value: cfg.reflectStyle, onChange: function (e) { set('reflectStyle', e.target.value) } },
5914
- STYLE_IDS.map(function (id) { return h('option', { key: id, value: id }, t('style' + id.charAt(0).toUpperCase() + id.slice(1))) })), t('fStyleHint')),
5915
- field(locale === 'zh' ? '子代理模型 / 思考强度' : 'Subagent model & reasoning effort', h('div', { 'data-dam-row': '', style: { flex: 1 } },
5916
- h('span', { style: { flex: 1, fontSize: 'calc(12px * var(--dam-scale))', wordBreak: 'break-all', opacity: cfg.subagentModel ? 1 : 0.6 } }, (cfg.subagentModel || (locale === 'zh' ? '跟随路由默认' : 'routing default')) + (String(cfg.subagentReasoningEffort || '') ? ' · ' + String(cfg.subagentReasoningEffort) : '')),
5917
- h('button', { 'data-dam-btn': '', onClick: openModels }, locale === 'zh' ? '选择模型 / 强度' : 'Pick model / effort')),
5918
- locale === 'zh' ? '用于时段总结、问候语、自动沉淀、蒸馏等 subagent 功能;模型与思考强度(off/low/high/max)都经 DSH 的 agentOptions 下发,留空跟随路由与模型默认。保存后生效。' : 'For scheduled summaries, greetings, auto-consolidation and distillation subagents; model and reasoning effort (off/low/high/max) are delivered via DSH agentOptions. Empty follows routing and model defaults. Applies after saving.'),
5919
- mdlOpen ? buildModelDrawer() : null,
6444
+ h('option', { value: 'relaxed' }, locale === 'zh' ? '舒展' : 'Relaxed'), h('option', { value: 'compact' }, locale === 'zh' ? '紧凑' : 'Compact')), locale === 'zh' ? '影响工作区关系图的节点间距和显示数量。' : 'Controls node spacing and detail in the workspace graph.'),
6445
+ // 群反馈第 4 条:排除来源(每行一条)。textarea 便于粘贴多行路径;
6446
+ // 空行/纯空白在写回时被过滤,避免"看起来配了其实没配"。
6447
+ field(t('fExclude'), h('textarea', {
6448
+ 'data-dam-input': '', rows: 3,
6449
+ style: { width: '100%', resize: 'vertical', fontFamily: 'inherit' },
6450
+ value: (Array.isArray(cfg.injectExcludeSources) ? cfg.injectExcludeSources : []).join('\n'),
6451
+ placeholder: 'mem_0123456789abcdef0123456789abcdef\nD:\\ws\\.dsh-memory\\archive\\\nlog',
6452
+ onChange: function (e) {
6453
+ var lines = String(e.target.value || '').split('\n').map(function (x) { return x.trim() }).filter(Boolean)
6454
+ set('injectExcludeSources', lines)
6455
+ },
6456
+ }), t('fExcludeHint')),
6457
+ field(locale === 'zh' ? '子代理模型 / 思考强度' : 'Subagent model & reasoning effort', h('div', { 'data-dam-row': '', style: { flex: 1 } },
6458
+ h('span', { style: { flex: 1, fontSize: 'calc(12px * var(--dam-scale))', wordBreak: 'break-all', opacity: cfg.subagentModel ? 1 : 0.6 } }, (cfg.subagentModel || (locale === 'zh' ? '跟随路由默认' : 'routing default')) + (String(cfg.subagentReasoningEffort || '') ? ' · ' + String(cfg.subagentReasoningEffort) : '')),
6459
+ h('button', { 'data-dam-btn': '', onClick: openModels }, locale === 'zh' ? '选择模型 / 强度' : 'Pick model / effort')),
6460
+ locale === 'zh' ? '用于时段总结、问候语、自动沉淀、蒸馏等 subagent 功能;模型与思考强度(off/low/high/max)都经 DSH 的 agentOptions 下发,留空跟随路由与模型默认。保存后生效。' : 'For scheduled summaries, greetings, auto-consolidation and distillation subagents; model and reasoning effort (off/low/high/max) are delivered via DSH agentOptions. Empty follows routing and model defaults. Applies after saving.'),
6461
+ mdlOpen ? buildModelDrawer() : null,
5920
6462
  ]),
5921
- // 2026-09-14 解耦:交接白板与自动接续各自独立成项(旧实现只在白板页有接续按钮,且 enabled 还被
5922
- // handoffEnabled 二次与运算)。两者出厂默认均为 false(功能仍在测试期),这里与白板页读写完全相同的配置键。
5923
- section('context', sectionLabels.context, [field(t('fHandoff'), h('input', { type: 'checkbox', 'data-dam-key': 'handoffEnabled', checked: cfg.handoffEnabled !== false, onChange: function (e) { set('handoffEnabled', e.target.checked) } }), t('fHandoffHint')),
5924
- field(t('fAutoContinue'), h('input', { type: 'checkbox', 'data-dam-key': 'autoContinueEnabled', checked: cfg.autoContinueEnabled !== false, onChange: function (e) { set('autoContinueEnabled', e.target.checked) } }), t('fAutoContinueHint')),
5925
- 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')),
5926
- 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')),
5927
- 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')),
5928
- 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')),
5929
- field(t('fWaterAdvisory'), h('input', { type: 'checkbox', checked: cfg.waterLevelAdvisory !== false, onChange: function (e) { set('waterLevelAdvisory', e.target.checked) } }), t('fWaterAdvisoryHint')),
5930
- field(t('fWaterAuto'), h('input', { type: 'checkbox', checked: cfg.waterLevelAutoHandoff !== false, onChange: function (e) { set('waterLevelAutoHandoff', e.target.checked) } }), t('fWaterAutoHint')),
5931
- // WB-GRAPH 白板线一键切换(2026-09-16, board_mode_v1): 旧版白板(默认) / 新版看板(dsh-graph vendor
5932
- // + sidecar + 遍历工具, 工具数 14→16)。开关解耦: 只管白板线; 切换需重启 dsh web 生效(工具注册在启动期)。
5933
- field(locale === 'zh' ? '白板模式(旧版白板 / 新版看板)' : 'Board mode (legacy / graph)', (function () {
5934
- var cur = cfg.boardMode === 'graph' ? 'graph' : 'legacy'
5935
- // ★2026-09-16 修 BUG-3(设置页切换不落盘): 旧实现 `set('boardMode', m)` 只改**本地 React 态**
5936
- // 然后 reload —— 配置从未写到服务端, 刷新后仍是旧值(界面回跳原档), 用户会判定"开关坏了"。
5937
- // 正解: 走与接续面板同一个写入路径 `saveConfigPatch(API.config, patch)`(见 :999 定义),
5938
- // 写盘成功后再 reload; 失败则在界面提示, 不静默假装成功。
5939
- // 同时满足用户硬性偏好: 开关类改动必须**即时回显**(写盘后立即 reload 呈现新档)。
5940
- function pick(m) {
5941
- set('boardMode', m) // 先本地回显(即时反馈)
5942
- saveConfigPatch({ boardMode: m }, {
5943
- onSaved: function () { window.setTimeout(function () { window.location.reload() }, 350) },
5944
- onError: function (e) { setErr(String((e && e.message) || e || 'save failed')) },
5945
- })
5946
- }
5947
- return h('div', { 'data-dam-row': '' },
5948
- h('button', { 'data-dam-btn': '', 'data-dam-key': 'boardMode', style: { fontWeight: cur === 'legacy' ? 700 : 400, opacity: cur === 'legacy' ? 1 : 0.65 }, onClick: function () { if (cur !== 'legacy') pick('legacy') } }, locale === 'zh' ? '旧版白板' : 'Legacy board'),
5949
- h('button', { 'data-dam-btn': '', 'data-dam-key': 'boardModeGraph', style: { marginLeft: '6px', fontWeight: cur === 'graph' ? 700 : 400, opacity: cur === 'graph' ? 1 : 0.65 }, onClick: function () { if (cur !== 'graph') pick('graph') } }, locale === 'zh' ? '新版看板(dsh-graph)' : 'Graph board (dsh-graph)'))
5950
- })(),
5951
- locale === 'zh' ? '一键切换白板形态。默认「旧版白板」=一切行为不变;切到「新版看板」后启用 dsh-graph 看板、白板结构化索引(handoff/index.json)与两个遍历工具(memory_expand / memory_trace,工具数 14→16)。切换后需重启 dsh web 生效;再点回旧版即完全回滚。' : 'One-click switch for the board form. Default "Legacy board" keeps everything unchanged; "Graph board" enables the dsh-graph kanban, the structured handoff index (handoff/index.json) and two traversal tools (memory_expand / memory_trace, tool count 14→16). Restart dsh web after switching; switch back to fully roll back.'),
5952
- field(locale === 'zh' ? '子代理痕迹回收' : 'Subagent trace recycle', h('input', { type: 'checkbox', checked: cfg.subagentGcEnabled !== false, onChange: function (e) { set('subagentGcEnabled', e.target.checked) } }),
5953
- locale === 'zh' ? '本插件的一次性子代理(自动沉淀 / 时段总结 / 问候 / 蒸馏)每跑一次都会在 ~/.dsh/sessions 留下一个持久化会话;数量上千后会话列表会明显变慢。开启后任务一结束就把这些痕迹移入 ~/.dsh/subagent-gc-backup 备份(可回滚),不影响子代理结果。默认开。' : 'Every one-shot subagent this plugin spawns (auto-consolidation, scheduled summary, greeting, distillation) leaves a persisted session under ~/.dsh/sessions; hundreds of them slow the session list down. When on, each trace is moved into ~/.dsh/subagent-gc-backup right after the task ends (reversible), never affecting the subagent result. On by default.'),
5954
- field(locale === 'zh' ? '兜底回收保留天数' : 'Fallback recycle keep days', h('input', { 'data-dam-input': '', type: 'number', min: 0, value: cfg.subagentGcKeepDays === undefined ? 3 : cfg.subagentGcKeepDays, onChange: function (e) { set('subagentGcKeepDays', Number(e.target.value) || 0) } }),
5955
- locale === 'zh' ? '每天巡检一次,回收超过该天数仍残留的痕迹(比如任务异常中断没删掉的);0 = 不按时间,只靠任务结束即删。' : 'A daily sweep recycles traces older than this many days (e.g. leftovers from interrupted tasks); 0 = rely on the end-of-task recycle only.')]),
5956
- section('maintenance', sectionLabels.maintenance, [field(t('fVersion'), h('div', { 'data-dam-row': '' },
5957
- h('span', { style: { flex: 1 } }, verInfo ? (verInfo.current || '?') + (verInfo.latest ? ' → ' + verInfo.latest + (verInfo.upToDate ? ' ' + t('upToDate') : ' ' + t('hasUpdate')) : '') : (checkingUpdate ? t('checking') : '—')),
5958
- h('button', { 'data-dam-btn': '', onClick: checkUpdate, disabled: checkingUpdate }, checkingUpdate ? t('checking') : t('checkUpdate')),
5959
- (verInfo && verInfo.latest && !verInfo.upToDate && verInfo.installKind === 'registry') ? h('button', { 'data-dam-btn': '', onClick: doUpdate, disabled: upBusy, style: { marginLeft: '4px' } }, upBusy ? t('updating') : t('updateNow')) : null,
5960
- upMsg ? h('span', { 'data-dam-hint': '' }, upMsg) : null),
5961
- t('versionCmdHint') + (verInfo && verInfo.error ? ' ' + t('versionError') + verInfo.error : '') + (verInfo && verInfo.installKind === 'dev-link' ? ' ' + t('devLinkHint') : '') + (verInfo && !verInfo.installKind ? ' ' + t('noProfileHint') : '')),
6463
+ section('about', sectionLabels.about, [
6464
+ // WB-GRAPH 白板线一键切换(2026-09-16, board_mode_v1): 旧版白板(默认) / 新版看板(dsh-graph vendor
6465
+ // + sidecar + 遍历工具, 工具数 14→16)。开关解耦: 只管白板线; 切换需重启 dsh web 生效(工具注册在启动期)。
6466
+ field(locale === 'zh' ? '白板模式(旧版白板 / 新版看板)' : 'Board mode (legacy / graph)', (function () {
6467
+ var cur = cfg.boardMode === 'graph' ? 'graph' : 'legacy'
6468
+ // ★2026-09-16 修 BUG-3(设置页切换不落盘): 旧实现 `set('boardMode', m)` 只改**本地 React 态**
6469
+ // 然后 reload —— 配置从未写到服务端, 刷新后仍是旧值(界面回跳原档), 用户会判定"开关坏了"。
6470
+ // 正解: 走与接续面板同一个写入路径 `saveConfigPatch(API.config, patch)`(见 :999 定义),
6471
+ // 写盘成功后再 reload; 失败则在界面提示, 不静默假装成功。
6472
+ // 同时满足用户硬性偏好: 开关类改动必须**即时回显**(写盘后立即 reload 呈现新档)。
6473
+ function pick(m) {
6474
+ set('boardMode', m) // 先本地回显(即时反馈)
6475
+ saveConfigPatch({ boardMode: m }, {
6476
+ onSaved: function () { window.setTimeout(function () { window.location.reload() }, 350) },
6477
+ onError: function (e) { setErr(String((e && e.message) || e || 'save failed')) },
6478
+ })
6479
+ }
6480
+ return h('div', { 'data-dam-row': '' },
6481
+ h('button', { 'data-dam-btn': '', 'data-dam-key': 'boardMode', style: { fontWeight: cur === 'legacy' ? 700 : 400, opacity: cur === 'legacy' ? 1 : 0.65 }, onClick: function () { if (cur !== 'legacy') pick('legacy') } }, locale === 'zh' ? '旧版白板' : 'Legacy board'),
6482
+ h('button', { 'data-dam-btn': '', 'data-dam-key': 'boardModeGraph', style: { marginLeft: '6px', fontWeight: cur === 'graph' ? 700 : 400, opacity: cur === 'graph' ? 1 : 0.65 }, onClick: function () { if (cur !== 'graph') pick('graph') } }, locale === 'zh' ? '新版看板(dsh-graph)' : 'Graph board (dsh-graph)'))
6483
+ })(),
6484
+ locale === 'zh' ? '一键切换白板形态。默认「旧版白板」=一切行为不变;切到「新版看板」后启用 dsh-graph 看板、白板结构化索引(handoff/index.json)与两个遍历工具(memory_expand / memory_trace,工具数 14→16)。切换后需重启 dsh web 生效;再点回旧版即完全回滚。' : 'One-click switch for the board form. Default "Legacy board" keeps everything unchanged; "Graph board" enables the dsh-graph kanban, the structured handoff index (handoff/index.json) and two traversal tools (memory_expand / memory_trace, tool count 14→16). Restart dsh web after switching; switch back to fully roll back.'),
6485
+ field(t('fVersion'), h('div', { 'data-dam-row': '' },
6486
+ h('span', { style: { flex: 1 } }, verInfo ? (verInfo.current || '?') + (verInfo.latest ? ' → ' + verInfo.latest + (verInfo.upToDate ? ' ' + t('upToDate') : ' ' + t('hasUpdate')) : '') : (checkingUpdate ? t('checking') : '—')),
6487
+ h('button', { 'data-dam-btn': '', onClick: checkUpdate, disabled: checkingUpdate }, checkingUpdate ? t('checking') : t('checkUpdate')),
6488
+ (verInfo && verInfo.latest && !verInfo.upToDate && verInfo.installKind === 'registry') ? h('button', { 'data-dam-btn': '', onClick: doUpdate, disabled: upBusy, style: { marginLeft: '4px' } }, upBusy ? t('updating') : t('updateNow')) : null,
6489
+ upMsg ? h('span', { 'data-dam-hint': '' }, upMsg) : null),
6490
+ t('versionCmdHint') + (verInfo && verInfo.error ? ' ' + t('versionError') + verInfo.error : '') + (verInfo && verInfo.installKind === 'dev-link' ? ' ' + t('devLinkHint') : '') + (verInfo && !verInfo.installKind ? ' ' + t('noProfileHint') : '')),
5962
6491
  field(locale === 'zh' ? '交流群' : 'Community', h('div', { 'data-dam-row': '' },
5963
6492
  h('span', { style: { flex: 1 } }, locale === 'zh' ? '反馈问题、交流使用技巧——加群响应比 issue 更快。' : 'Share feedback and tips with other users — the group responds faster than GitHub issues.'),
5964
6493
  h('a', { href: 'https://qm.qq.com/q/v7Asxn6vPa', target: '_blank', rel: 'noreferrer', style: { textDecoration: 'none', color: 'var(--dam-accent, #4f7cff)' } }, locale === 'zh' ? '点击加入 QQ 交流群' : 'Join the QQ community group')),
5965
- locale === 'zh' ? '点击链接一键加群。' : 'One-click join via the QQ invite link.')]),
6494
+ locale === 'zh' ? '点击链接一键加群。' : 'One-click join via the QQ invite link.'),
6495
+ ]),
5966
6496
  h('div', { 'data-dam-savebar': '' },
5967
6497
  h('button', { 'data-dam-btn': '', 'data-dirty': dirty ? 'true' : undefined, onClick: save, disabled: busy }, busy ? t('saving') : (dirty ? (locale === 'zh' ? '保存更改' : 'Save changes') : t('saveSettings'))),
5968
6498
  dirty ? h('span', { 'data-dam-hint': '' }, locale === 'zh' ? '有未保存的更改' : 'Unsaved changes') : null,
@@ -6236,6 +6766,19 @@ window.__ModuleLoader__.load({
6236
6766
  surfaceDisposers.push(slots.inject('settings.section', function () {
6237
6767
  return slots.register({ name: 'settings.section', id: 'auto-memory', order: 25, label: t('autoMemory') + ' (pre)' }, function (props) { return h(SettingsPage, { close: props && props.close }) })
6238
6768
  }))
6769
+ // ★2026-09-21 二次修正:「记忆」会话页 —— 与「对话轨迹」「上下文」「白板看板」并列的一页。
6770
+ // 用户实测反馈:「它现在是浮在整个页面上方的,而不是和对话轨迹、上下文、白板看板在一起,作为一个单独的一页」。
6771
+ // 根因:宿主只给 4 个槽位(sidebar/main/rightbar/shell.overlay),overlay 里的东西只能"盖"在界面上;
6772
+ // 要成为并列页必须走 conversation.view(官方对话轨迹与本插件白板看板都注册在这里)。
6773
+ // 出现条件:承载面 = 会话页 / 两者共存;切回「左下角浮层」时移除该页签(由 refreshSurfaces 重注册)。
6774
+ if (controller.panelPos() === 'page' || controller.panelPos() === 'both') {
6775
+ surfaceDisposers.push(slots.inject('conversation.view', function () {
6776
+ return slots.register(
6777
+ { name: 'conversation.view', id: 'auto-memory-panel', order: 78, label: function () { return locale === 'zh' ? '记忆' : 'Memory' } },
6778
+ function (props) { return h(MemoryPageView, props || {}) },
6779
+ )
6780
+ }))
6781
+ }
6239
6782
  // 2026-09-16「双承载面」: 会话页顶栏再挂一个**整页白板看板**(与侧边面板共存)。
6240
6783
  // 面板 440px 装不下矩阵, 整页 ~1000px+ 可以 ⇒ 两者按容器宽度各用其形态。
6241
6784
  // 与 boardMode 闸门解耦: 组件内部自己取数, 非 graph 档拿到 enabled:false 就显示提示,
@@ -6276,8 +6819,12 @@ window.__ModuleLoader__.load({
6276
6819
  surfaceDisposers = []
6277
6820
  registerSurfaces()
6278
6821
  }
6822
+ // 把「重注册承载面」暴露给模块作用域:设置页切换承载面时,「记忆」会话页签要当场增/删(见 setSurfacesRefreshHook)
6823
+ setSurfacesRefreshHook(refreshSurfaces)
6824
+ // ★2026-09-21 二次修正:syncTitlebarVar() 及其调用点已一并删除 —— 它只为「顶部通栏覆盖条」让位 Windows
6825
+ // 自绘标题栏而存在,该形态废弃后无消费者(会话页在正常文档流里,由宿主布局负责)。
6279
6826
  registerSurfaces()
6280
- console.log('[dsh-auto-memory] client ready: sidebar entry + panel + settings page')
6827
+ console.log('[dsh-auto-memory] client ready: sidebar entry + floating card / memory session page (switchable) + settings page')
6281
6828
  }
6282
6829
 
6283
6830
  exports.inject = ['slots', 'sessions', 'remote', 'remote.session']