@a9i5k4/dsh-auto-memory 2.2.3 → 2.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -25,6 +25,10 @@ window.__ModuleLoader__.load({
25
25
  var panelOpen = false
26
26
  var panelClosing = false
27
27
  var closeTimer = null
28
+ // 悬浮钉(2026-09-08):钉住后点击面板外不再自动收起,适合边看记忆边操作桌面
29
+ var PIN_KEY = 'dsh-auto-memory.panel.pinned'
30
+ var pinned = false
31
+ try { pinned = localStorage.getItem(PIN_KEY) === '1' } catch (ePin) {}
28
32
  // 几何状态:left/top/width/height,持久化到 localStorage(用户可拖动/缩放)
29
33
  var GEOM_KEY = 'dsh-auto-memory.panel.geom'
30
34
  var DEFAULT_W = 440
@@ -133,6 +137,12 @@ window.__ModuleLoader__.load({
133
137
  }, 170)
134
138
  },
135
139
  subscribe: function (fn) { listeners.add(fn); return function () { listeners.delete(fn) } },
140
+ isPinned: function () { return pinned },
141
+ togglePin: function () {
142
+ pinned = !pinned
143
+ try { localStorage.setItem(PIN_KEY, pinned ? '1' : '0') } catch (e) {}
144
+ emit()
145
+ },
136
146
  }
137
147
 
138
148
  // ───────────────────────── 国际化 ─────────────────────────
@@ -194,7 +204,7 @@ window.__ModuleLoader__.load({
194
204
  kvMem: '记忆文件', kvMemUser: '用户', kvMemNotes: '笔记', kvMemLog: '日志', kvMemMissing: '缺失',
195
205
  kvWs: '当前工作区', kvWsUnknown: '(未取到)', kvApi: 'API 连通', unknown: '(未知)',
196
206
  updateTitle: 'dsh-auto-memory 已更新', updateSub: '本次更新内容:', gotIt: '知道了', noticeOpen: '了解更多',
197
- refresh: '刷新', refreshing: '刷新中…', close: '关闭', dragMove: '拖动移动', dragResize: '拖动调整大小', resetPos: '恢复默认位置',
207
+ refresh: '刷新', refreshing: '刷新中…', close: '关闭', dragMove: '拖动移动', dragResize: '拖动调整大小', resetPos: '恢复默认位置', pinPanel: '钉住面板(点桌面其他地方不收起)', unpinPanel: '取消钉住(点面板外自动收起)',
198
208
  generated: '已生成', failed: '失败: ',
199
209
  generatedAt: '生成于 ',
200
210
  pointsCount: ' 项', summarizing: 'AI 总结生成中…', summaryFailed: 'AI 总结生成失败,点 ⟳ 重试',
@@ -262,7 +272,7 @@ window.__ModuleLoader__.load({
262
272
  fWaterWindow: '水位估计窗口(token)', fWaterWindowHint: '会话消息按官方公式(4 字符≈1 token)估算达到该值即视为上下文将满;0=关闭。默认 65536(128K 窗口的保守半量),按所用模型调整。',
263
273
  fWaterAdvisory: '水位交接建议', fWaterAdvisoryHint: '水位越阈时在动态快照注入交接建议(写账本/刷新白板/建议开新窗);无人值守时静默。',
264
274
  fWaterAuto: '水位自动骨架账本', fWaterAutoHint: '越阈时自动写一篇系统骨架账本(每会话一次),防止模型忽视建议时交接材料缺失。',
265
- fWaterThreshold: '水位建议阈值', fWaterThresholdHint: '水位比例超过该值(0.1-1.5)即触发交接建议/骨架账本;默认 0.8。',
275
+ fWaterThreshold: '水位建议阈值', fWaterThresholdHint: '水位比例超过该值(0.1-1.5)即触发交接建议/骨架账本;默认 0.75——官方自动压缩阈值是 80%,必须留出余量才来得及走完交接。',
266
276
  fHandoffPlan: '白板注入预算(字符)', fHandoffPlanHint: 'PLAN.md 全貌注入动态快照的硬截断预算;全文可经 memory_read 或面板白板页查看。默认 1200。',
267
277
  fHandoffLedger: '账本注入预算(字符)', fHandoffLedgerHint: '最新一篇交接账本注入动态快照的硬截断预算。默认 800。',
268
278
  fConsSchedule: '定时做梦式固化', fConsScheduleHint: '每天到点自动读最近日志发散提炼长期要点,写入项目笔记/用户级记忆(等价 memory_consolidate)。',
@@ -270,8 +280,8 @@ window.__ModuleLoader__.load({
270
280
  fConsScheduleDays: '固化回看天数', fConsScheduleDaysHint: '定时固化读取最近 N 天日志;默认 7。',
271
281
  fMaintSchedule: '定时 30 天蒸馏', fMaintScheduleHint: '每天到点自动把超过 30 天的旧日志蒸馏归档(等价 memory_maintain);无旧日志时零成本跳过。',
272
282
  fMaintScheduleTime: '蒸馏触发时间', fMaintScheduleTimeHint: 'HH:MM,默认 10:00(与固化时间错开)。',
273
- waterCardTitle: '上下文水位', waterAutoSrc: '自动检测', waterManualSrc: '手动设定', waterMeterOfficial: '官方计量', waterMeterHeuristic: '启发式降级', waterThresholdHint: '水位达到 {t} 时注入交接建议并自动补写账本;计量优先用官方 token-meter(与聊天框 context ring 同源),不可用时降级启发式估算;窗口大小自动取自当前模型(settings.yaml contextWindow),也可在下方手动覆盖。',
274
- autoContTitle: '自动接续', autoContHint: '开启后:水位达到阈值且会话空闲(连续两轮水位不增长)时,倒计时 30 秒自动执行一键接续(总结→写账本→同工作区开新会话→注入接续材料);触发后 30 分钟内不重复;随时可取消。', autoContOn: '自动接续:开', autoContOff: '自动接续:关', autoContThreshold: '阈值', autoContCountdown: '⏳ {s} 秒后自动接续(总结并开新会话)…', autoContCancel: '取消',
283
+ waterCardTitle: '上下文水位', waterAutoSrc: '自动检测', waterManualSrc: '手动设定', waterOfficialSrc: '官方路由容量', waterFallbackSrc: '回退默认值', waterNotMeasured: '本会话尚未测量(发送一轮后自动更新)', waterMeterOfficial: '官方计量', waterMeterHeuristic: '启发式降级', waterThresholdHint: '水位达到 {t} 时注入交接建议并自动补写账本;计量优先用官方 token-meter(与聊天框 context ring 同源),不可用时降级启发式估算;窗口优先取官方路由容量(request/context 的 contextWindow),其次按当前模型查 settings.yaml contextWindow,也可在下方手动覆盖。',
284
+ autoContTitle: '自动接续', autoContHint: '开启后:水位达到阈值、且旧会话已空闲(harness 权威 running 位在轮次边界转为 false,取代旧「两轮水位持平」)时,弹出确认卡——同意=立即接续,拒绝=本轮跳过(同一边界不再提示),30-40 秒无操作视为挂机自动接续;接续前先让旧 Agent 刷新白板 PLAN 与交接账本,再建新会话(沿用工作区/模型/思考档位)并注入分层交接材料;触发后默认 30 分钟内不重复。', autoContOn: '自动接续:开', autoContOff: '自动接续:关', autoContThreshold: '阈值', autoContCountdown: '⏳ {s} 秒后自动接续…', autoContCancel: '取消', autoContConfirm: '本会话水位已达 {p}%,旧会话已空闲(轮次边界)——现在接续到新会话?', autoContAgree: '同意接续', autoContReject: '拒绝', autoContTimeout: '{s} 秒后自动接续(视为挂机)', autoContRejected: '已跳过本次接续(下一个轮次边界再提醒)',
275
285
  continueCardTitle: '一键接续', continueCardHint: '写好交接账本→创建新会话→把交接材料作为首条消息预载,并自动切换到新会话继续任务(接续材料含白板节选+最新账本;当前工作区无账本时自动取全局最近一篇)。', continueBtn: '一键接续到新会话', continueBusy: '处理中…',
276
286
  fStyle: '反思风格', fStyleHint: '生活化 / 专业性 / 由内容决定。',
277
287
  fLocale: '界面语言', fLocaleHint: '默认跟随 DSH 系统语言;也可手动指定中文 / English。', followSystem: '跟随系统语言',
@@ -342,7 +352,7 @@ window.__ModuleLoader__.load({
342
352
  kvMem: 'Memory files', kvMemUser: 'user', kvMemNotes: 'notes', kvMemLog: 'log', kvMemMissing: 'missing',
343
353
  kvWs: 'Current workspace', kvWsUnknown: '(unknown)', kvApi: 'API probes', unknown: '(unknown)',
344
354
  updateTitle: 'dsh-auto-memory Updated', updateSub: 'What\'s new in this update:', gotIt: 'Got it', noticeOpen: 'Learn more',
345
- refresh: 'Refresh', refreshing: 'Refreshing…', close: 'Close', dragMove: 'Drag to move', dragResize: 'Drag to resize', resetPos: 'Reset position',
355
+ refresh: 'Refresh', refreshing: 'Refreshing…', close: 'Close', dragMove: 'Drag to move', dragResize: 'Drag to resize', resetPos: 'Reset position', pinPanel: 'Pin panel (stays open when clicking elsewhere)', unpinPanel: 'Unpin (auto-hide on outside click)',
346
356
  generated: 'Generated', failed: 'Failed: ',
347
357
  generatedAt: 'Generated ',
348
358
  pointsCount: ' items', summarizing: 'AI summary generating…', summaryFailed: 'Summary failed, press refresh to retry',
@@ -410,7 +420,7 @@ window.__ModuleLoader__.load({
410
420
  fWaterWindow: 'Water-level window (tokens)', fWaterWindowHint: 'Session messages priced with the official meter (4 chars ≈ 1 token); past this, context is considered nearly full; 0 disables. Default 65536 — a conservative half of a 128K window.',
411
421
  fWaterAdvisory: 'Water-level handoff advisory', fWaterAdvisoryHint: 'Injects a handoff advisory past the threshold (write ledger / refresh plan / suggest a new window); silent in unattended mode.',
412
422
  fWaterAuto: 'Auto skeleton ledger', fWaterAutoHint: 'Past the threshold, auto-writes one system skeleton ledger per session as a safety net if the model ignores the advisory.',
413
- fWaterThreshold: 'Water-level threshold', fWaterThresholdHint: 'Inject the handoff advisory / skeleton ledger once the ratio passes this value (0.1-1.5); default 0.8.',
423
+ fWaterThreshold: 'Water-level threshold', fWaterThresholdHint: 'Inject the handoff advisory / skeleton ledger once the ratio passes this value (0.1-1.5); default 0.75 — the official auto-compaction threshold is 80%, so a 5% margin is needed to finish the handoff first.',
414
424
  fHandoffPlan: 'Plan injection budget (chars)', fHandoffPlanHint: 'Hard truncation budget for injecting the PLAN.md snapshot into the dynamic memory; full text via memory_read or the Whiteboard tab. Default 1200.',
415
425
  fHandoffLedger: 'Ledger injection budget (chars)', fHandoffLedgerHint: 'Hard truncation budget for injecting the latest handoff ledger. Default 800.',
416
426
  fConsSchedule: 'Scheduled deep consolidation', fConsScheduleHint: 'Once a day at the set time, read recent logs and distill long-term value into project notes / user memory (same flow as memory_consolidate).',
@@ -418,8 +428,8 @@ window.__ModuleLoader__.load({
418
428
  fConsScheduleDays: 'Consolidation lookback (days)', fConsScheduleDaysHint: 'How many recent days of logs the scheduled consolidation reads; default 7.',
419
429
  fMaintSchedule: 'Scheduled 30-day distill', fMaintScheduleHint: 'Once a day at the set time, distill & archive logs older than 30 days (same flow as memory_maintain); zero cost when nothing to archive.',
420
430
  fMaintScheduleTime: 'Distill time', fMaintScheduleTimeHint: 'HH:MM, default 10:00 (staggered from the consolidation time).',
421
- waterCardTitle: 'Context water level', waterAutoSrc: 'auto-detected', waterManualSrc: 'manual', waterMeterOfficial: 'official meter', waterMeterHeuristic: 'heuristic fallback', waterThresholdHint: 'Past {t} of the window, the handoff advisory is injected and the skeleton ledger is backfilled; metering prefers the official token-meter (same source as the chat context ring), falling back to the heuristic when unavailable; the window auto-follows the active model (settings.yaml contextWindow) or your manual override below.',
422
- autoContTitle: 'Auto continue', autoContHint: 'When on: once the water level passes the threshold and the session is idle (level unchanged across two polls), a 30s countdown auto-runs One-click continue (summarize → write ledger → open a new session in the same workspace → inject the handoff prompt); suppressed for 30 minutes after each run; cancel anytime.', autoContOn: 'Auto continue: on', autoContOff: 'Auto continue: off', autoContThreshold: 'Threshold', autoContCountdown: '⏳ Auto-continue in {s}s (summarize & open new session)…', autoContCancel: 'Cancel',
431
+ waterCardTitle: 'Context water level', waterAutoSrc: 'auto-detected', waterManualSrc: 'manual', waterOfficialSrc: 'route capacity', waterFallbackSrc: 'fallback default', waterNotMeasured: 'Not measured for this session yet (updates after the next turn)', waterMeterOfficial: 'official meter', waterMeterHeuristic: 'heuristic fallback', waterThresholdHint: 'Past {t} of the window, the handoff advisory is injected and the skeleton ledger is backfilled; metering prefers the official token-meter (same source as the chat context ring), falling back to the heuristic when unavailable; the window prefers the official route capacity (request/context contextWindow), then the active model in settings.yaml, or your manual override below.',
432
+ autoContTitle: 'Auto continue', autoContHint: 'When on: once the water level passes the threshold AND the old session is idle (the authoritative harness running bit flipped to false at a turn boundary, replacing the old "level unchanged across two polls"), a confirm card appears — Agree = continue now, Decline = skip this boundary (no re-prompt for it), no answer for 30-40s = unattended, continue automatically. Before continuing, the old agent refreshes PLAN + ledger; the new session keeps workspace/model/reasoning effort and receives layered handoff material; suppressed for 30 minutes after each run.', autoContOn: 'Auto continue: on', autoContOff: 'Auto continue: off', autoContThreshold: 'Threshold', autoContCountdown: '⏳ Auto-continue in {s}s…', autoContCancel: 'Cancel', autoContConfirm: 'Water level at {p}% and the old session is idle (turn boundary) — continue into a new session now?', autoContAgree: 'Continue', autoContReject: 'Decline', autoContTimeout: 'auto-continue in {s}s (unattended)', autoContRejected: 'Skipped this handoff (will ask again at the next turn boundary)',
423
433
  continueCardTitle: 'One-click continue', continueCardHint: 'Writes the handoff ledger → creates a new session → injects the handoff material as the first message → switches to the new session automatically (material = PLAN excerpt + latest ledger; falls back to the most recent global ledger when this workspace has none).', continueBtn: 'Continue in a new session', continueBusy: 'Working…',
424
434
  fStyle: 'Reflection style', fStyleHint: 'Life-style / Professional / Auto.',
425
435
  fLocale: 'UI language', fLocaleHint: 'Follows the DSH system language by default; you can also pin Chinese / English.', followSystem: 'Follow system language',
@@ -446,6 +456,21 @@ window.__ModuleLoader__.load({
446
456
  }
447
457
  var sessions = null
448
458
  var remoteFace = null
459
+ // 官方 session.create 返回形态多样(裸字符串 / {sessionId} / {id} / {value} / {ok,value:{sessionId}}),统一提取会话 id
460
+ function extractSessionId(created) {
461
+ if (created == null) return null
462
+ if (typeof created === 'string') return created
463
+ if (typeof created !== 'object') return null
464
+ if (typeof created.sessionId === 'string') return created.sessionId
465
+ if (typeof created.id === 'string') return created.id
466
+ var v = created.ok && created.value !== undefined ? created.value : (created.value !== undefined ? created.value : null)
467
+ if (v && typeof v === 'string') return v
468
+ if (v && typeof v === 'object') {
469
+ if (typeof v.sessionId === 'string') return v.sessionId
470
+ if (typeof v.id === 'string') return v.id
471
+ }
472
+ return null
473
+ }
449
474
  // 当前会话工作区(跟随 GUI 切换工作区):从 sessions 服务拿,供 state/summarize/greet 请求使用
450
475
  function currentWs() {
451
476
  try {
@@ -457,6 +482,41 @@ window.__ModuleLoader__.load({
457
482
  return (s && typeof s.cwd === 'string') ? s.cwd : ''
458
483
  } catch (e) { return '' }
459
484
  }
485
+ // 轮次边界探测(①,2026-09-08 2.2.4):读 harness 权威 running 位(sessions.list 快照,官方 buildListSnapshot 投影),
486
+ // 取代旧「连续两轮水位不增长=空闲」——长工具调用会被误判空闲、真挂机又永不触发。
487
+ function currentRunningInfo() {
488
+ try {
489
+ var snap = sessions && sessions.list && sessions.list.getSnapshot()
490
+ if (!snap) return null
491
+ var id = snap.current
492
+ if (id === undefined || id === null) return null
493
+ var s = snap.byId && snap.byId[id]
494
+ if (!s) return null
495
+ return { sessionId: id, running: !!s.running }
496
+ } catch (e) { return null }
497
+ }
498
+ function runningOfSession(sessionId) {
499
+ try {
500
+ var snap = sessions && sessions.list && sessions.list.getSnapshot()
501
+ var s = snap && snap.byId ? snap.byId[sessionId] : null
502
+ return s ? !!s.running : null
503
+ } catch (e) { return null }
504
+ }
505
+ // 当前会话 id(跟随 GUI 切换会话):水位卡按会话取数,切换即刷新(2026-09-08)
506
+ function currentSessionIdClient() {
507
+ try {
508
+ var snap = sessions && sessions.list && sessions.list.getSnapshot()
509
+ if (!snap) return ''
510
+ var id = snap.current
511
+ return (id === undefined || id === null) ? '' : String(id)
512
+ } catch (e) { return '' }
513
+ }
514
+ function amCtzValue() {
515
+ return (typeof Intl !== 'undefined' && typeof Intl.DateTimeFormat === 'function') ? (Intl.DateTimeFormat().resolvedOptions().timeZone || undefined) : undefined
516
+ }
517
+ function amReqId(prefix) {
518
+ return (window.crypto && typeof crypto.randomUUID === 'function') ? crypto.randomUUID() : (prefix || 'amc') + '-' + Date.now() + '-' + Math.random().toString(36).slice(2)
519
+ }
460
520
  var fontScale = 'lg'
461
521
  var FONT_SCALES = { sm: '小', md: '标准', lg: '大', xl: '特大' }
462
522
  // 与服务端 lib/index.js DEFAULT_PROMPT_LAYERS 保持一致(设置页显示各层默认文案)
@@ -501,6 +561,37 @@ window.__ModuleLoader__.load({
501
561
 
502
562
  // ───────────────────────── 更新弹窗 / 首次指导 ─────────────────────────
503
563
  var CHANGELOG = {
564
+ '2.2.5': { zh: [
565
+ '★ 修复:上下文水位百分比不再被硬截断到 150%——旧版 `Math.min(tokens / window, 1.5)` 让任何超额都显示成同一个数(例如 761,692 / 131,072 真实为 581%)。现在如实上报,进度条仍按 100% 封顶。',
566
+ '★ 修复:水位卡标出的模型现在是「当前会话真实使用的模型」——此前一律读 settings.yaml 的 agent-default-model(新会话默认模型),你在界面切过模型后,卡片会张冠李戴地标成另一个模型(实测会话跑 deepseek-official/deepseek-v4.1-flash,却标成 opencode-go/deepseek-v4-flash)。现改为优先取会话日志 request/header 的 provider/model,默认模型只作兜底。',
567
+ '★ 修复:水位卡改成按会话显示——此前是全机单值、只在有对话轮次时更新,切到别的会话仍显示上一个会话的数(或重启后的 0),模型名也只能拿默认模型凑。现在按 sessionId 取该会话的实测值;没有实测就现场从该会话日志推导窗口与模型,并明确标注「本会话尚未测量」。',
568
+ '★ 版本号同步:面板徽标与「检测更新」读的是开发树的 package.json,长期停在 0.1.30(而 npm 已是 2.2.x),于是界面显示旧版本、更新检查永远报「有新版本」。现在发布脚本会把版本号回写开发树,两条线永远一致。',
569
+ '★ 新增悬浮钉:记忆面板标题栏加了一个图钉按钮(线描图标,与 ⟳ ⤾ ✕ 同画风),点一下钉住——之后再点桌面或其他地方,面板不会自动收起(再点一下取消)。钉住状态会记住。',
570
+ '★ 交接阈值下调到 75%:官方自动压缩阈值是 80%,阈值贴着 80% 常常还没走完交接就被官方压缩掉;水位建议阈值与自动接续阈值默认都改为 0.75,留 5% 余量(1M 窗口约 50K token)。已存在的 0.8 配置不会被自动改写,可在设置页手动改。',
571
+ ], en: [
572
+ '★ Fix: the context water-level percentage is no longer clamped to 150% — the old `Math.min(tokens / window, 1.5)` made every over-window reading look identical (761,692 / 131,072 is really 581%). It now reports the true value; the progress bar still caps at 100%.',
573
+ '★ Fix: the model shown on the water-level card is now the model the session is actually using — it used to always read agent-default-model from settings.yaml, so after switching models in the UI the card named a different model (observed: session running deepseek-official/deepseek-v4.1-flash but the card said opencode-go/deepseek-v4-flash). It now prefers provider/model from the session log\'s request/header, with the default model only as a fallback.',
574
+ '★ Fix: the water-level card is now per-session — it used to be a single global value updated only on turns, so switching conversations kept showing another session\'s numbers (or 0 after a restart) with the default model name. It now reads the measured value for the session id, and when there is none it derives the window/model from that session log and says "not measured for this session yet".',
575
+ '★ Version sync: the panel badge and "Check for updates" read the dev tree package.json, which sat at 0.1.30 while npm was already 2.2.x — the UI showed an old version and the update check always reported "update available". The release script now writes the version back to the dev tree so both lines stay in lockstep.',
576
+ '★ New pin button: the panel header now has a line-art pin toggle matching the ⟳ ⤾ ✕ icons — pin it and clicking elsewhere on the desktop no longer auto-hides the panel (click again to unpin). The pinned state is remembered.',
577
+ '★ Handoff threshold lowered to 75%: the official auto-compaction threshold is 80%, so a threshold sitting at 80% often gets compacted before the handoff finishes. Both the water-level advisory and the auto-continue threshold now default to 0.75, leaving 5% headroom (~50K tokens on a 1M window). An existing 0.8 in your config is not rewritten automatically — change it in Settings if you want the new default.',
578
+ ] },
579
+ '2.2.4': { zh: [
580
+ '★ 修复:一键接续的新会话终于落进旧会话所属工作区——官方 session.create 只有传 workspaceId 才会绑定工作区,只传 cwd 仅设工作目录(故此前恒落「未分组」);现 host 先解析旧会话的 workspaceId 再建会话,解析不到才回退 cwd。',
581
+ '★ 修复:新会话沿用旧会话的模型与思考档位——模型信息在会话日志的 request/header.header.config(旧版查错了事件类型,恒取空 → 回落默认模型)。',
582
+ '★ 自动接续 v3:触发条件改为「水位≥阈值 且 harness 权威 running==false(轮次边界)」,弃用「两轮水位持平」——长工具调用不再误触发,真挂机一定触发;达阈值先弹确认卡:同意=立即接续 / 拒绝=本轮跳过(同一边界不再提示)/ 30-40 秒无操作=视为挂机自动接续。',
583
+ '★ 接续前刷新仪式:确认后先让旧 Agent 把白板 PLAN 与交接账本刷到最新,再用最新材料组装交接(可在配置里关掉);新窗口不再拿到过时白板。',
584
+ '★ 修复:上下文水位虚高(显示「500,998 / 131,072 token · 150%」)——模型窗口解析两条路同时失效:① settings.yaml 的 llm-deepseek 段是 flow 风格 YAML,旧解析器只认 block 风格;② 官方 request/context 的 contextWindow 只在会话开头出现(实测 2999 条事件里仅 2 条),旧代码只扫最近 256 条 → 1M 窗口被当成 128K。现在优先取官方路由容量、全量扫描并按会话缓存,设置页如实标出「官方路由容量 / 回退默认值」,百分比也不再截断到 150%。',
585
+ '★ 接续材料分层压缩:第0层指令+白板 / 第1层交接账本 / 第2层近期线程(最近 20 条 × 700 字,保留角色与工具标记)/ 第3层完整转写按需 read;材料带时间戳与「白板比账本旧」过期提示。',
586
+ '★ 新增子代理痕迹回收:自动沉淀 / 时段总结 / 问候 / 蒸馏等一次性子代理每跑一次都会在 ~/.dsh/sessions 下留下一个持久化会话(实测全机 686 个子代理会话里 638 个来自本插件),积累上千后会拖慢会话列表;现在子代理一结束就把痕迹移入 ~/.dsh/subagent-gc-backup(只移动不删除,可回滚),另有每日兜底巡检,设置页可开关并调整保留天数。',
587
+ ], en: [
588
+ '★ Fix: one-click continue now lands the new session in the old session\'s workspace — the official session.create only binds a workspace when workspaceId is passed (cwd alone just sets the working directory, which is why it always landed in "Ungrouped"); the host now resolves the old session\'s workspaceId first and only falls back to cwd.',
589
+ '★ Fix: the new session now inherits the old session\'s model and reasoning effort — that info lives in request/header.header.config (the old code read the wrong event type and always got nothing, falling back to the default model).',
590
+ '★ Auto-continue v3: triggers on "water level ≥ threshold AND the authoritative harness running bit flipped to false (turn boundary)" instead of "level unchanged across two polls" — long tool calls no longer misfire and genuinely unattended sessions always fire; a confirm card appears first: Agree = continue now, Decline = skip this boundary (no re-prompt), 30-40s of silence = unattended, continue automatically.',
591
+ '★ Refresh ritual before continuing: the old agent refreshes PLAN + ledger first so the new session never starts from a stale whiteboard (configurable).',
592
+ '★ Layered handoff material: layer 0 instructions + PLAN / layer 1 ledger / layer 2 recent thread (last 20 entries × 700 chars, roles and tool markers kept) / layer 3 full transcript read on demand; timestamps and a "PLAN older than ledger" staleness hint included.',
593
+ '★ New subagent trace recycle: every one-shot subagent this plugin spawns (auto-consolidation, scheduled summary, greeting, distillation) left a persisted session under ~/.dsh/sessions (638 of the 686 subagent sessions on this machine came from this plugin), and enough of them slow the session list down. Each trace is now moved into ~/.dsh/subagent-gc-backup right after the task ends (moved, never deleted — fully reversible), plus a daily fallback sweep; toggle and retention days live in Settings.',
594
+ ] },
504
595
  '2.2.3': { zh: [
505
596
  '打包修复:npm 包 files 现包含 docs/,用户文档随包发布——README 的「📖 用户文档」链接在 GitHub 与 npm 包页均可打开。',
506
597
  ], en: [
@@ -819,6 +910,7 @@ window.__ModuleLoader__.load({
819
910
  '[data-dam-resize]:hover { opacity: 1; }',
820
911
  '[data-dam-btn] { border: none; background: transparent; cursor: pointer; color: inherit; opacity: .75; font-size: calc(13px * var(--dam-scale)); padding: 4px 8px; border-radius: 6px; }',
821
912
  '[data-dam-btn]:hover { opacity: 1; background: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 16%, transparent); }',
913
+ '[data-dam-btn][data-pin="on"] { opacity: 1; background: color-mix(in srgb, var(--dam-accent, #4f7cff) 24%, transparent); }',
822
914
  '[data-dam-tabs-wrap] { display: flex; align-items: center; min-width: 0; border-bottom: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.2)) 55%, transparent); }',
823
915
  '[data-dam-tabs] { flex: 1; min-width: 0; overflow: hidden; padding: 5px 4px; scrollbar-width: none; -ms-overflow-style: none; }',
824
916
  '[data-dam-tabs]::-webkit-scrollbar { display: none; }',
@@ -880,7 +972,7 @@ window.__ModuleLoader__.load({
880
972
  '[data-dam-banner] { border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #e6a23c) 55%, transparent);',
881
973
  ' background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #e6a23c) 13%, transparent); border-radius: 9px; padding: 8px 10px; margin-bottom: 10px; font-size: calc(12.5px * var(--dam-scale)); }',
882
974
  '[data-dam-input], [data-dam-select] { width: 100%; box-sizing: border-box; background: color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.08)) 45%, transparent); color: inherit; border: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.3)) 60%, transparent); border-radius: 7px; padding: 6px 8px; font: inherit; }',
883
- '[data-dam-row] { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }',
975
+ '[data-dam-row] { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; }',
884
976
  '[data-dam-row] label { flex: 0 0 110px; opacity: .8; font-size: calc(12.5px * var(--dam-scale)); }',
885
977
  '[data-dam-hint] { opacity: .5; font-size: calc(11.5px * var(--dam-scale)); margin-top: 2px; }',
886
978
  '[data-dam-sidebar-btn] { display: flex; align-items: center; gap: 6px; width: 100%; border: none; background: transparent; color: inherit; cursor: pointer; padding: 6px 10px; border-radius: 8px; font: inherit; font-size: 13px; opacity: .8; }',
@@ -1121,6 +1213,22 @@ window.__ModuleLoader__.load({
1121
1213
  var label = props && props.label ? props.label : t('loading')
1122
1214
  return h('div', { 'data-dam-loading': '' }, h('span', { 'data-dam-spinner': '', 'aria-hidden': 'true' }), h('span', null, label))
1123
1215
  }
1216
+ /** 线描图钉图标(2026-09-08):与标题栏 ⤾ ⟳ ✕ 同一画风——单色 currentColor 描边、无彩色 emoji;
1217
+ * 未钉住=描边空心,已钉住=填充(内孔用 evenodd 挖空)。 */
1218
+ function PinIcon(props) {
1219
+ var filled = !!(props && props.filled)
1220
+ return h('svg', {
1221
+ width: 13, height: 13, viewBox: '0 0 16 16', 'aria-hidden': 'true',
1222
+ style: { display: 'block', margin: '0 auto', pointerEvents: 'none' },
1223
+ }, h('path', {
1224
+ d: 'M8 1.7c2.2 0 4 1.8 4 4 0 2.9-4 8.6-4 8.6S4 8.6 4 5.7c0-2.2 1.8-4 4-4zM8 4.2a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z',
1225
+ fill: filled ? 'currentColor' : 'none',
1226
+ fillRule: 'evenodd',
1227
+ stroke: 'currentColor',
1228
+ strokeWidth: '1.1',
1229
+ strokeLinejoin: 'round',
1230
+ }))
1231
+ }
1124
1232
  function AnimatedDisclosure(props) {
1125
1233
  var open = !!props.open
1126
1234
  var shownPair = useState(open)
@@ -1855,12 +1963,110 @@ window.__ModuleLoader__.load({
1855
1963
  }
1856
1964
 
1857
1965
  // M-CM6-C 自动接续跨渲染状态(apply 作用域,插件生命周期存活;组件重挂不重置)
1858
- var autoState = { cdIv: null, lastTokens: null }
1966
+ // v3(2.2.4):lastRunning/edgeAt=轮次边界;rejectedEdgeAt=拒绝后同一边界不重复提示;suppressUntil=接续进行中不重复触发
1967
+ var autoState = { cdIv: null, lastTokens: null, lastRunning: null, edgeAt: 0, edgeSession: '', rejectedEdgeAt: 0, promptVisible: false, suppressUntil: 0 }
1968
+
1969
+ // ───────── 接续执行器(一键接续 + 自动接续共用,2026-09-08 2.2.4)─────────
1970
+ // 流程:③刷新仪式(旧 Agent 先刷 PLAN+账本)→ 取材料 → 修A(workspaceId 建会话)→ 序号标题
1971
+ // → 修B(沿用模型+思考档位)→ 打开 → 注入材料。
1972
+ var continueFlowBusy = false
1973
+ function continueCreateArgs(d) {
1974
+ // 修A(2026-09-08):官方 session.create 只接受 workspaceId 或 cwd 之一(同传报 bad-request),
1975
+ // 且只有 workspaceId 分支会 workspace.attachSession()(api-session-controller/lib/index.js:566-588);
1976
+ // 只传 cwd 仅设工作目录 → 新会话永远落「未分组工作区」。故优先 workspaceId,解析不到才回退 cwd。
1977
+ if (d && d.workspaceId) return { workspaceId: d.workspaceId, agentPreset: d.agentPreset || undefined }
1978
+ return d && d.ws ? { cwd: d.ws, agentPreset: d.agentPreset || undefined } : { agentPreset: (d && d.agentPreset) || undefined }
1979
+ }
1980
+ async function refreshOldSession(onMsg) {
1981
+ // ③接续前刷新仪式:让旧 Agent 把 PLAN.md 与交接账本刷到最新,host 再用最新材料组装交接。
1982
+ // 任何失败/超时都 fail-soft(用现有材料继续),绝不阻塞接续。
1983
+ try {
1984
+ var st = await apiGet(API.handoffState)
1985
+ var rf = st && st.refresh
1986
+ if (!rf || !rf.sessionId || !rf.prompt) return { ok: false, skipped: true }
1987
+ if (!remoteFace || !remoteFace.session || typeof remoteFace.session.prompt !== 'function') return { ok: false, skipped: true }
1988
+ var before = String((st.planMtime || 0)) + '|' + String((st.ledgers && st.ledgers[0] && st.ledgers[0].name) || '')
1989
+ if (onMsg) onMsg(locale === 'zh' ? '刷新仪式:请旧 Agent 更新白板 PLAN 与交接账本…' : 'Refresh ritual: asking the old agent to update PLAN + ledger…')
1990
+ await remoteFace.session.prompt({ requestId: amReqId('amr'), sessionId: rf.sessionId, mode: 'queue', content: [{ type: 'text', text: rf.prompt }], clientTimeZone: amCtzValue() })
1991
+ var waited = await waitForRefresh(rf.sessionId, before)
1992
+ return { ok: true, waited: waited }
1993
+ } catch (eRf) { return { ok: false, error: String(eRf && eRf.message ? eRf.message : eRf) } }
1994
+ }
1995
+ async function waitForRefresh(oldId, before) {
1996
+ // 结束条件(任一):材料变了 / 旧会话跑完这一轮(true→false) / 超时(默认 90s,配置可调)。
1997
+ var t0 = Date.now()
1998
+ var timeoutMs = 90000
1999
+ try {
2000
+ var cfg = await apiGet(API.config)
2001
+ var sec = cfg && Number(cfg.autoContinueRefreshTimeoutSeconds)
2002
+ if (sec >= 15 && sec <= 600) timeoutMs = sec * 1000
2003
+ } catch (eC) {}
2004
+ var sawRunning = false
2005
+ while (Date.now() - t0 < timeoutMs) {
2006
+ await new Promise(function (r) { setTimeout(r, 2500) })
2007
+ var run = runningOfSession(oldId)
2008
+ if (run === true) sawRunning = true
2009
+ var st2 = null
2010
+ try { st2 = await apiGet(API.handoffState) } catch (eG) {}
2011
+ var now = String((st2 && st2.planMtime) || 0) + '|' + String((st2 && st2.ledgers && st2.ledgers[0] && st2.ledgers[0].name) || '')
2012
+ if (now !== before) return 'updated'
2013
+ if (sawRunning && run === false) return 'turn-ended'
2014
+ }
2015
+ return 'timeout'
2016
+ }
2017
+ async function executeContinue(d, onMsg) {
2018
+ var rf = remoteFace
2019
+ if (!rf || !rf.session || typeof rf.session.create !== 'function') throw new Error(locale === 'zh' ? 'harness 未提供 remote.session(版本过旧?)' : 'remote.session unavailable (harness too old?)')
2020
+ if (onMsg) onMsg(locale === 'zh' ? '正在创建新会话(沿用旧工作区与模型)…' : 'Creating session (same workspace & model)…')
2021
+ var created
2022
+ try { created = await rf.session.create(continueCreateArgs(d)) } catch (eCreate) {
2023
+ // workspaceId 已失效(工作区被删)或旧 harness 不认 → 回退 cwd,再退纯 agentPreset
2024
+ created = await rf.session.create(d && d.ws ? { cwd: d.ws, agentPreset: d.agentPreset || undefined } : { agentPreset: (d && d.agentPreset) || undefined })
2025
+ }
2026
+ // 官方 create 返回裸 SessionId 字符串(而非对象),兼容字符串/{sessionId}/{id}/{value}/{ok,value}
2027
+ var newId = extractSessionId(created)
2028
+ if (!newId) throw new Error('create: no sessionId')
2029
+ // 接续序号标题:让用户一眼区分新会话 vs 延续会话
2030
+ if (d.contSeq && rf.session.rename && typeof rf.session.rename === 'function') {
2031
+ var contTitle = (locale === 'zh' ? '接续 #' : 'Cont.#') + String(d.contSeq) + (d.wsBase ? ' · ' + d.wsBase : '')
2032
+ try { await rf.session.rename({ sessionId: newId, title: contTitle }) } catch (eRen) {}
2033
+ }
2034
+ // 沿用旧会话模型(provider/model/reasoningEffort 由 host 从旧会话 request/header 折叠而来;不可用则静默回退路由默认)
2035
+ if (d.provider && d.model && typeof rf.session.selectModel === 'function') {
2036
+ if (onMsg) onMsg(locale === 'zh' ? '正在沿用旧模型 ' + d.model + (d.reasoningEffort ? '(' + d.reasoningEffort + ')' : '') + '…' : 'Restoring model ' + d.model + '…')
2037
+ try { await rf.session.selectModel({ sessionId: newId, provider: d.provider, model: d.model, reasoningEffort: d.reasoningEffort || undefined }) } catch (eMdl) {}
2038
+ }
2039
+ try { if (sessions && typeof sessions.open === 'function') sessions.open(newId) } catch (eOpen) {}
2040
+ await rf.session.prompt({ requestId: amReqId('amc'), sessionId: newId, mode: 'queue', content: [{ type: 'text', text: d.carryText }], clientTimeZone: amCtzValue() })
2041
+ return newId
2042
+ }
2043
+ async function runContinueFlow(opts) {
2044
+ var onMsg = (opts && opts.onProgress) || function () {}
2045
+ if (continueFlowBusy) return { ok: false, error: 'busy' }
2046
+ continueFlowBusy = true
2047
+ autoState.suppressUntil = Date.now() + 5 * 60 * 1000
2048
+ try {
2049
+ var cfg = null
2050
+ try { cfg = await apiGet(API.config) } catch (eCfg) {}
2051
+ var ritual = !(cfg && cfg.autoContinueRefreshRitual === false) && !(opts && opts.skipRefresh)
2052
+ if (ritual) { try { await refreshOldSession(onMsg) } catch (eRf) {} }
2053
+ onMsg(locale === 'zh' ? '正在构造交接材料(含旧会话转写)…' : 'Preparing handoff material (incl. prev-session transcript)…')
2054
+ var d = await apiPost(API.handoffContinue, {})
2055
+ if (!d || !d.ok) throw new Error((d && d.error) || 'no handoff material')
2056
+ var newId = await executeContinue(d, onMsg)
2057
+ try { localStorage.setItem('dsh-auto-memory.autoCont.last', String(Date.now())) } catch (eL) {}
2058
+ return { ok: true, sessionId: newId, workspaceId: d.workspaceId || '', model: d.model || '', reasoningEffort: d.reasoningEffort || '' }
2059
+ } finally { continueFlowBusy = false }
2060
+ }
1859
2061
 
1860
2062
  function PlanTab() {
1861
2063
  var dataPair = useState(null)
1862
2064
  var data = dataPair[0]
1863
2065
  var setData = dataPair[1]
2066
+ // 当前会话 id:水位卡按会话取数(切换会话即刷新,2026-09-08)
2067
+ var sidPair = useState(function () { return currentSessionIdClient() })
2068
+ var sid = sidPair[0]
2069
+ var setSid = sidPair[1]
1864
2070
  var openPair = useState(null)
1865
2071
  var open = openPair[0]
1866
2072
  var setOpen = openPair[1]
@@ -1874,71 +2080,48 @@ window.__ModuleLoader__.load({
1874
2080
  var contMsgPair = useState('')
1875
2081
  var contMsg = contMsgPair[0]
1876
2082
  var setContMsg = contMsgPair[1]
1877
- // M-CM6-C 自动接续(水位阈值触发,默认关;轮询 handoff-state,连续两轮水位不增长视为会话空闲)
2083
+ // M-CM6-C 自动接续(①轮次边界触发 + ②确认卡由全局 AutoContinueHost 执行;这里只读/写开关与阈值)
1878
2084
  var autoPair = useState(null)
1879
2085
  var autoCfg = autoPair[0]
1880
2086
  var setAutoCfg = autoPair[1]
1881
- var cdPair = useState(0)
1882
- var cd = cdPair[0]
1883
- var setCd = cdPair[1]
1884
2087
  useEffect(function () {
1885
2088
  apiGet(API.config).then(function (c) {
1886
- setAutoCfg({ enabled: !(c && c.autoContinueEnabled === false), threshold: (c && Number(c.autoContinueThreshold)) || 0.8 })
2089
+ setAutoCfg({ enabled: !(c && c.autoContinueEnabled === false), threshold: (c && Number(c.autoContinueThreshold)) || 0.75 })
1887
2090
  }).catch(function () { setAutoCfg({ enabled: false, threshold: 0.7 }) })
1888
2091
  }, [])
1889
2092
  var autoSave = function (patch) {
1890
2093
  setAutoCfg(function (p) { return Object.assign({}, p, patch) })
1891
2094
  apiPost(API.config, patch).catch(function () {})
1892
2095
  }
1893
- var autoCancel = function () {
1894
- if (autoState.cdIv) { clearInterval(autoState.cdIv); autoState.cdIv = null }
1895
- setCd(0)
1896
- }
1897
- var autoStartCountdown = function () {
1898
- if (autoState.cdIv) return
1899
- var left = 30
1900
- setCd(left)
1901
- autoState.cdIv = setInterval(function () {
1902
- left -= 1
1903
- if (left <= 0) {
1904
- clearInterval(autoState.cdIv); autoState.cdIv = null; setCd(0)
1905
- try { localStorage.setItem('dsh-auto-memory.autoCont.last', String(Date.now())) } catch (eL) {}
1906
- oneClickContinue()
1907
- } else setCd(left)
1908
- }, 1000)
1909
- }
1910
2096
  async function oneClickContinue() {
2097
+ // 一键接续 = 共用执行器(刷新仪式 → 材料 → workspaceId 建会话 → 沿用模型 → 注入),与自动接续同一路径
1911
2098
  if (contBusy) return
1912
2099
  setContBusy(true)
1913
- setContMsg(locale === 'zh' ? '正在构造交接材料(含旧会话转写)…' : 'Preparing handoff material (incl. prev-session transcript)…')
1914
2100
  try {
1915
- var d = await apiPost(API.handoffContinue, {})
1916
- if (!d || !d.ok) throw new Error((d && d.error) || 'no handoff material')
1917
- var rf = remoteFace
1918
- if (!rf || !rf.session || typeof rf.session.create !== 'function') throw new Error(locale === 'zh' ? 'harness 未提供 remote.session(版本过旧?)' : 'remote.session unavailable (harness too old?)')
1919
- setContMsg(locale === 'zh' ? '正在创建新会话(沿用旧工作区与权限)…' : 'Creating session (same workspace & permissions)…')
1920
- // M-CM6-B v2:同 cwd 创建 → 工作区绑定/权限状态与旧会话一致;失败回退无参创建
1921
- var created
1922
- try { created = await rf.session.create(d.ws ? { cwd: d.ws } : {}) } catch (eCwd) { created = await rf.session.create({}) }
1923
- var newId = created && (created.sessionId || created.id)
1924
- if (!newId) throw new Error('create: no sessionId')
1925
- // 沿用旧会话模型(provider/model 由 host 从旧会话 request/context 折叠而来;不可用则静默回退路由默认)
1926
- if (d.provider && d.model && typeof rf.session.selectModel === 'function') {
1927
- setContMsg(locale === 'zh' ? '正在沿用旧模型 ' + d.model + '…' : 'Restoring model ' + d.model + '…')
1928
- try { await rf.session.selectModel({ sessionId: newId, provider: d.provider, model: d.model }) } catch (eMdl) {}
1929
- }
1930
- try { if (sessions && typeof sessions.open === 'function') sessions.open(newId) } catch (eOpen) {}
1931
- var reqId = (window.crypto && typeof crypto.randomUUID === 'function') ? crypto.randomUUID() : 'amc-' + Date.now() + '-' + Math.random().toString(36).slice(2)
1932
- await rf.session.prompt({ requestId: reqId, sessionId: newId, mode: 'queue', content: [{ type: 'text', text: d.carryText }] })
1933
- setContMsg(locale === 'zh' ? '✓ 已创建新会话(模型/工作区已沿用)并预载交接材料' : '✓ Session created (model/workspace restored) with ledger injected')
2101
+ var r = await runContinueFlow({ onProgress: setContMsg })
2102
+ setContMsg((locale === 'zh' ? '✓ 已创建新会话(工作区/模型已沿用)并预载交接材料' : '✓ Session created (workspace/model restored) with ledger injected') + (r && r.model ? ' · ' + r.model + (r.reasoningEffort ? '/' + r.reasoningEffort : '') : ''))
1934
2103
  } catch (e) {
1935
2104
  setContMsg('✗ ' + String(e && e.message ? e.message : e))
1936
2105
  } finally { setContBusy(false) }
1937
2106
  }
1938
2107
  useEffect(function () {
1939
2108
  var alive = true
1940
- apiGet(API.handoffState).then(function (d) { if (alive) setData(d) }).catch(function () {})
2109
+ setData(null)
2110
+ apiGet(API.handoffState, { sessionId: sid }).then(function (d) { if (alive) setData(d) }).catch(function () {})
1941
2111
  return function () { alive = false }
2112
+ }, [sid])
2113
+ // 切换会话即刷新水位卡:订阅官方会话列表快照,current 变化时重取(2026-09-08)
2114
+ useEffect(function () {
2115
+ var unsub = null
2116
+ try {
2117
+ if (sessions && sessions.list && typeof sessions.list.subscribe === 'function') {
2118
+ unsub = sessions.list.subscribe(function () {
2119
+ var now = currentSessionIdClient()
2120
+ setSid(function (prev) { return prev === now ? prev : now })
2121
+ })
2122
+ }
2123
+ } catch (eSub) {}
2124
+ return function () { if (typeof unsub === 'function') unsub() }
1942
2125
  }, [])
1943
2126
  useEffect(function () {
1944
2127
  if (!open) return
@@ -1946,55 +2129,47 @@ window.__ModuleLoader__.load({
1946
2129
  apiGet(API.handoffState, { file: open }).then(function (d) { if (alive) setContent(d.text || '') }).catch(function () {})
1947
2130
  return function () { alive = false }
1948
2131
  }, [open])
1949
- useEffect(function () {
1950
- if (!autoCfg || !autoCfg.enabled) return
1951
- var alive = true
1952
- var tick = function () {
1953
- apiGet(API.handoffState).then(function (d) {
1954
- if (!alive || !d || !d.enabled) return
1955
- var wl = d.waterLevel || {}
1956
- if (!wl.window || !(wl.ratio >= (autoCfg.threshold || 0.7))) { autoState.lastTokens = null; return }
1957
- var idle = autoState.lastTokens !== null && wl.tokens === autoState.lastTokens
1958
- autoState.lastTokens = wl.tokens
1959
- if (!idle) return
1960
- var last = 0
1961
- try { last = Number(localStorage.getItem('dsh-auto-memory.autoCont.last') || 0) } catch (eL) {}
1962
- if (Date.now() - last < 30 * 60 * 1000) return
1963
- autoStartCountdown()
1964
- }).catch(function () {})
1965
- }
1966
- var iv = setInterval(tick, 20000)
1967
- tick()
1968
- return function () { alive = false; clearInterval(iv) }
1969
- }, [autoCfg && autoCfg.enabled, autoCfg && autoCfg.threshold])
2132
+ // 自动触发已移交全局 AutoContinueHost(①轮次边界 + ②确认卡);面板这里只保留手动一键接续与开关/阈值。
1970
2133
  useEffect(function () {
1971
2134
  return function () { if (autoState.cdIv) { clearInterval(autoState.cdIv); autoState.cdIv = null } }
1972
2135
  }, [])
2136
+ // 账本/计划版本按钮:窄面板下长文件名会被折成竖排,只显示短标签(时间),完整名放悬浮提示
2137
+ function shortLedgerName(name) {
2138
+ var m = /^(handoff|PLAN)-(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})\.md$/.exec(name || '')
2139
+ if (m) { var t = m[5] + ':' + m[6] + (m[1] === 'handoff' ? ':' + m[7] : ''); return (m[1] === 'PLAN' ? 'PLAN ' : '') + m[3] + '-' + m[4] + ' ' + t }
2140
+ var p = /^prev-session-.+-(\d{2})(\d{2})(\d{2})\.md$/.exec(name || '')
2141
+ if (p) return '↻ ' + p[1] + ':' + p[2] + ':' + p[3]
2142
+ return name
2143
+ }
1973
2144
  if (!data) return h(Loading)
1974
2145
  if (!data.enabled) return h(Card, { title: t('planTitle') }, h('div', { 'data-dam-hint': '' }, t('planDisabled')))
1975
2146
  var wl = data.waterLevel || {}
1976
2147
  var rows = [h(Card, { title: t('planTitle') }, h('div', { 'data-dam-content': '' }, data.plan || t('planEmpty')))]
1977
2148
  if (wl.window > 0) {
1978
- var pct = Math.min(Math.round((wl.ratio || 0) * 100), 150)
1979
- var barColor = pct >= 80 ? '#d4744f' : pct >= 50 ? '#d4a94f' : 'var(--dam-accent, #4f7cff)'
2149
+ // 2.2.4:百分比显示真实水位(旧版截断到 150% 会把「窗口算错」伪装成「刚好超一点」);进度条宽度仍按 100% 截断。
2150
+ var rawPct = Math.round((wl.ratio || 0) * 100)
2151
+ var pct = Math.min(rawPct, 999)
2152
+ var barColor = rawPct >= 80 ? '#d4744f' : rawPct >= 50 ? '#d4a94f' : 'var(--dam-accent, #4f7cff)'
2153
+ var srcLabel = !wl.source ? '' : wl.source.indexOf('auto') === 0 ? t('waterAutoSrc') + ': ' + wl.source.slice(5) : (wl.source === 'official-context' ? t('waterOfficialSrc') : (wl.source === 'fallback' ? t('waterFallbackSrc') : t('waterManualSrc')))
1980
2154
  rows.unshift(h(Card, { title: t('waterCardTitle') }, h('div', { 'data-dam-content': '' }, [
1981
2155
  h('div', { style: { fontSize: 'calc(12px * var(--dam-scale))', marginBottom: '6px' } },
1982
2156
  (wl.tokens || 0).toLocaleString() + ' / ' + wl.window.toLocaleString() + ' token · ' + pct + '%',
1983
- ' ', h('span', { 'data-dam-hint': '' }, wl.source ? (wl.source.indexOf('auto') === 0 ? t('waterAutoSrc') + ': ' + wl.source.slice(5) : t('waterManualSrc')) : ''),
2157
+ ' ', h('span', { 'data-dam-hint': '' }, srcLabel),
1984
2158
  ' ', h('span', { 'data-dam-hint': '' }, wl.meter ? (wl.meter.indexOf('official') === 0 ? '· ' + t('waterMeterOfficial') + '(' + String(wl.meter).slice(9) + ')' : '· ' + t('waterMeterHeuristic')) : '')),
2159
+ // 本会话尚未测量(刚重启/刚切过来还没发消息):明说,而不是显示成 0 让人以为上下文是空的
2160
+ wl.at ? null : h('div', { 'data-dam-hint': '', style: { marginBottom: '4px' } }, t('waterNotMeasured')),
1985
2161
  h('div', { style: { height: '6px', borderRadius: '3px', background: 'color-mix(in srgb, currentColor 12%, transparent)', overflow: 'hidden' } },
1986
2162
  h('div', { style: { height: '100%', width: Math.min(pct, 100) + '%', background: barColor, borderRadius: '3px' } })),
1987
- h('div', { 'data-dam-hint': '', style: { marginTop: '4px' } }, t('waterThresholdHint').replace('{t}', String(wl.threshold || 0.8))),
2163
+ h('div', { 'data-dam-hint': '', style: { marginTop: '4px' } }, t('waterThresholdHint').replace('{t}', String(wl.threshold || 0.75))),
1988
2164
  ])))
1989
2165
  }
1990
2166
  if (autoCfg && wl.window > 0) {
1991
2167
  rows.unshift(h(Card, { title: t('autoContTitle') }, h('div', { 'data-dam-content': '' }, [
1992
2168
  h('div', { 'data-dam-hint': '', style: { marginBottom: '6px' } }, t('autoContHint')),
1993
2169
  h('div', { 'data-dam-row': '' },
1994
- h('button', { 'data-dam-btn': '', onClick: function () { autoCancel(); autoSave({ autoContinueEnabled: !autoCfg.enabled }) } }, autoCfg.enabled ? t('autoContOn') : t('autoContOff')),
2170
+ h('button', { 'data-dam-btn': '', onClick: function () { autoSave({ autoContinueEnabled: !autoCfg.enabled }) } }, autoCfg.enabled ? t('autoContOn') : t('autoContOff')),
1995
2171
  h('label', { 'data-dam-hint': '', style: { marginLeft: '8px' } }, t('autoContThreshold') + ' ',
1996
2172
  h('input', { 'data-dam-input': '', type: 'number', min: 0.5, max: 0.95, step: 0.05, value: String(autoCfg.threshold), style: { width: '64px' }, onChange: function (e) { var v = Number(e.target.value); if (v >= 0.5 && v <= 0.95) autoSave({ autoContinueThreshold: v }) } }))),
1997
- cd > 0 ? h('div', { 'data-dam-hint': '', style: { marginTop: '6px', color: '#d4744f' } }, t('autoContCountdown').replace('{s}', String(cd)) + ' ', h('button', { 'data-dam-btn': '', onClick: autoCancel }, t('autoContCancel'))) : null,
1998
2173
  ])))
1999
2174
  }
2000
2175
  if (open) {
@@ -2003,11 +2178,11 @@ window.__ModuleLoader__.load({
2003
2178
  } else {
2004
2179
  if (data.planVersions && data.planVersions.length) {
2005
2180
  rows.push(h(Card, { title: t('planVersions') }, h('div', { 'data-dam-row': '' }, data.planVersions.map(function (v) {
2006
- return h('button', { 'data-dam-btn': '', key: v.name, onClick: function () { setOpen(v.name) } }, v.name)
2181
+ return h('button', { 'data-dam-btn': '', key: v.name, title: v.name, onClick: function () { setOpen(v.name) } }, shortLedgerName(v.name))
2007
2182
  }))))
2008
2183
  }
2009
2184
  rows.push(h(Card, { title: t('planLedgers') }, h('div', { 'data-dam-row': '' }, (data.ledgers || []).length ? data.ledgers.map(function (v) {
2010
- return h('button', { 'data-dam-btn': '', key: v.name, onClick: function () { setOpen(v.name) } }, v.name)
2185
+ return h('button', { 'data-dam-btn': '', key: v.name, title: v.name, onClick: function () { setOpen(v.name) } }, shortLedgerName(v.name))
2011
2186
  }) : h('div', { 'data-dam-muted': '' }, t('empty')))))
2012
2187
  // M-CM6-B 一键接续:写账本→建新会话→预载交接材料→自动切换
2013
2188
  rows.push(h(Card, { title: t('continueCardTitle') }, h('div', { 'data-dam-content': '' }, [
@@ -2708,6 +2883,14 @@ window.__ModuleLoader__.load({
2708
2883
  h('strong', null, t('autoMemory')),
2709
2884
  h('span', { 'data-dam-hint': '', style: { fontSize: '10px', opacity: .55 } }, ver ? 'v' + ver + '-ui' : ''),
2710
2885
  h('span', { className: 'dam-spacer' }),
2886
+ // 悬浮钉(2026-09-08):钉住后点面板外不再自动收起;图标为线描 SVG,与 ⤾ ⟳ ✕ 同画风
2887
+ h('button', {
2888
+ 'data-dam-btn': '', 'data-pin': pinned ? 'on' : 'off',
2889
+ title: pinned ? t('unpinPanel') : t('pinPanel'),
2890
+ 'aria-pressed': pinned ? 'true' : 'false',
2891
+ style: { display: 'inline-flex', alignItems: 'center', justifyContent: 'center', padding: '4px 6px' },
2892
+ onClick: function () { controller.togglePin() },
2893
+ }, h(PinIcon, { filled: pinned })),
2711
2894
  h('button', { 'data-dam-btn': '', title: t('resetPos'), onClick: function () { controller.resetGeom() } }, '⤾'),
2712
2895
  h('button', { 'data-dam-btn': '', title: t('refresh'), onClick: function () { setNonce(nonce + 1) } }, '⟳'),
2713
2896
  h('button', { 'data-dam-btn': '', title: t('close'), onClick: function () { controller.close() } }, '✕')),
@@ -3286,9 +3469,10 @@ window.__ModuleLoader__.load({
3286
3469
  // ───────────────────────── 设置页 ─────────────────────────
3287
3470
  var STYLE_IDS = ['auto', 'life', 'professional']
3288
3471
  var LOCALE_IDS_LIST = ['system', 'zh', 'en']
3289
- // M-CM6-C v2·常驻自动接续 watcher(2026-09-08):全局生效,与面板/页签是否打开无关。
3290
- // 条件:autoContinueEnabled !== false(缺省=开)&& 水位≥阈值(缺省0.8)&& 连续两轮 tokens 不增长(会话空闲)
3291
- // && 距上次触发>30min → 30s 可取消倒计时 → 自动执行接续(写账本→建会话→切换→注入材料)。
3472
+ // M-CM6-C v3·常驻自动接续 watcher(2026-09-08 2.2.4):全局生效,与面板/页签是否打开无关。
3473
+ // ①触发 = autoContinueEnabled !== false && 水位≥阈值 && harness 权威 running==false(轮次边界,取代旧"两轮水位持平")
3474
+ // + 3s 短确认(防刚结束又起新一轮)→ ②确认卡(同意 / 拒绝 / 超时 N 秒自动接续;拒绝记录在案,同一边界不再提示)
3475
+ // → ③刷新仪式(旧 Agent 刷 PLAN+账本)→ 一键接续(workspaceId 建会话 → 沿用模型 → 注入材料)。
3292
3476
  function AutoContinueHost() {
3293
3477
  var cfgP = useState(null)
3294
3478
  var acCfg = cfgP[0]
@@ -3302,6 +3486,9 @@ window.__ModuleLoader__.load({
3302
3486
  var bsP = useState(false)
3303
3487
  var acBusy = bsP[0]
3304
3488
  var setAcBusy = bsP[1]
3489
+ var cfP = useState(null)
3490
+ var acConfirm = cfP[0]
3491
+ var setAcConfirm = cfP[1]
3305
3492
  useEffect(function () {
3306
3493
  var alive = true
3307
3494
  var load = function () {
@@ -3314,74 +3501,116 @@ window.__ModuleLoader__.load({
3314
3501
  useEffect(function () {
3315
3502
  var on = !(acCfg && acCfg.autoContinueEnabled === false)
3316
3503
  if (!on) return
3317
- var thr = (acCfg && Number(acCfg.autoContinueThreshold)) || 0.8
3504
+ var thr = (acCfg && Number(acCfg.autoContinueThreshold)) || 0.75
3505
+ var confirmSec = Number(acCfg && acCfg.autoContinueConfirmSeconds)
3506
+ if (!(confirmSec >= 10 && confirmSec <= 120)) confirmSec = 35
3507
+ var cooldownMin = Number(acCfg && acCfg.autoContinueCooldownMinutes)
3508
+ if (!(cooldownMin > 0)) cooldownMin = 30
3318
3509
  var alive = true
3319
- var tick = function () {
3320
- apiGet(API.handoffState).then(function (d) {
3510
+ var maybePrompt = function (edgeAt) {
3511
+ if (!alive || acBusy || autoState.cdIv || autoState.promptVisible) return
3512
+ if (!edgeAt || Date.now() - edgeAt > 10 * 60 * 1000) return // 边界太旧不再提示
3513
+ if (Date.now() < autoState.suppressUntil) return
3514
+ if (autoState.rejectedEdgeAt === edgeAt) return // ②拒绝过同一边界
3515
+ apiGet(API.handoffState, { sessionId: currentSessionIdClient() }).then(function (d) {
3321
3516
  if (!alive || !d || !d.enabled) return
3322
3517
  var wl = d.waterLevel || {}
3323
- if (!wl.window || !(wl.ratio >= thr)) { autoState.lastTokens = null; return }
3324
- var idle = autoState.lastTokens !== null && wl.tokens === autoState.lastTokens
3325
- autoState.lastTokens = wl.tokens
3326
- if (!idle || autoState.cdIv || acBusy) return
3518
+ if (!wl.window || !(wl.ratio >= thr)) return
3327
3519
  var last = 0
3328
3520
  try { last = Number(localStorage.getItem('dsh-auto-memory.autoCont.last') || 0) } catch (eL) {}
3329
- if (Date.now() - last < 30 * 60 * 1000) return
3330
- setAcSt(null)
3331
- var left = 30
3521
+ if (Date.now() - last < cooldownMin * 60 * 1000) return
3522
+ if (autoState.promptVisible || autoState.cdIv) return
3523
+ setAcConfirm({ ratio: Number(wl.ratio) || 0, tokens: Number(wl.tokens) || 0, window: Number(wl.window) || 0, edgeAt: edgeAt })
3524
+ setAcSt('')
3525
+ autoState.promptVisible = true
3526
+ var left = confirmSec
3332
3527
  setAcCd(left)
3333
3528
  autoState.cdIv = setInterval(function () {
3334
3529
  left -= 1
3335
3530
  if (left <= 0) {
3336
3531
  clearInterval(autoState.cdIv); autoState.cdIv = null; setAcCd(0)
3337
- runAutoContinue()
3532
+ autoState.promptVisible = false; setAcConfirm(null)
3533
+ runAuto() // ③超时 = 挂机 → 自动接续兜底
3338
3534
  } else setAcCd(left)
3339
3535
  }, 1000)
3340
3536
  }).catch(function () {})
3341
3537
  }
3342
- var iv = setInterval(tick, 20000)
3343
- tick()
3344
- return function () { alive = false; clearInterval(iv) }
3345
- }, [acCfg && acCfg.autoContinueEnabled === false, acCfg && (Number(acCfg.autoContinueThreshold) || 0.8), acBusy])
3538
+ // 轮次边界:running true→false 边沿 + 3s 短确认
3539
+ var verifyEdge = function (edgeAt) {
3540
+ if (!alive || autoState.edgeAt !== edgeAt) return
3541
+ var info = currentRunningInfo()
3542
+ if (info && info.running) return // 短确认失败:新一轮已开始,不算边界
3543
+ maybePrompt(edgeAt)
3544
+ }
3545
+ var armEdge = function () {
3546
+ autoState.edgeAt = Date.now()
3547
+ var info = currentRunningInfo()
3548
+ autoState.edgeSession = info ? info.sessionId : ''
3549
+ var edgeAt = autoState.edgeAt
3550
+ setTimeout(function () { verifyEdge(edgeAt) }, 3000)
3551
+ }
3552
+ var observe = function () {
3553
+ if (!alive) return
3554
+ var info = currentRunningInfo()
3555
+ if (!info) return
3556
+ if (autoState.lastRunning === null) {
3557
+ autoState.lastRunning = info.running
3558
+ if (!info.running) armEdge() // 首次观察即空闲(挂机/刚进会话):按边界处理
3559
+ return
3560
+ }
3561
+ if (autoState.lastRunning === true && info.running === false) armEdge()
3562
+ autoState.lastRunning = info.running
3563
+ }
3564
+ var unsub = null
3565
+ try { if (sessions && sessions.list && typeof sessions.list.subscribe === 'function') unsub = sessions.list.subscribe(observe) } catch (eSub) {}
3566
+ observe()
3567
+ var iv = setInterval(function () { observe(); maybePrompt(autoState.edgeAt) }, 20000) // 兜底:水位后到 / 订阅不可用
3568
+ return function () {
3569
+ alive = false
3570
+ clearInterval(iv)
3571
+ if (unsub) { try { unsub() } catch (eU) {} }
3572
+ }
3573
+ }, [acCfg && acCfg.autoContinueEnabled === false, acCfg && (Number(acCfg.autoContinueThreshold) || 0.75), acCfg && acCfg.autoContinueConfirmSeconds, acCfg && acCfg.autoContinueCooldownMinutes, acBusy])
3346
3574
  var cancelAc = function () {
3347
3575
  if (autoState.cdIv) { clearInterval(autoState.cdIv); autoState.cdIv = null }
3576
+ autoState.promptVisible = false
3348
3577
  setAcCd(0)
3349
3578
  }
3350
- var runAutoContinue = function () {
3579
+ var runAuto = function () {
3351
3580
  if (acBusy) return
3581
+ cancelAc()
3582
+ setAcConfirm(null)
3352
3583
  if (!remoteFace || !remoteFace.session || typeof remoteFace.session.create !== 'function') {
3353
3584
  setAcSt(locale === 'zh' ? '✗ remote.session 不可用——请重启 dsh web 后重试' : '✗ remote.session unavailable — restart dsh web')
3354
3585
  return
3355
3586
  }
3356
3587
  setAcBusy(true)
3357
- setAcSt(locale === 'zh' ? '正在自动接续:写账本→建新会话→注入材料…' : 'Auto-continuing: ledger → new session → inject…')
3358
- apiPost(API.handoffContinue, {}).then(function (d) {
3359
- if (!d || !d.ok) throw new Error((d && d.error) || 'no handoff material')
3360
- var p
3361
- try { p = remoteFace.session.create(d.ws ? { cwd: d.ws } : {}) } catch (eC) { p = remoteFace.session.create({}) }
3362
- return p.then(function (created) {
3363
- var newId = created && (created.sessionId || created.id)
3364
- if (!newId) throw new Error('create: no sessionId')
3365
- var chain = Promise.resolve()
3366
- if (d.provider && d.model && typeof remoteFace.session.selectModel === 'function') {
3367
- chain = chain.then(function () { try { return remoteFace.session.selectModel({ sessionId: newId, provider: d.provider, model: d.model }) } catch (eM) { return null } })
3368
- }
3369
- return chain.then(function () {
3370
- try { if (sessions && typeof sessions.open === 'function') sessions.open(newId) } catch (eO) {}
3371
- var reqId = (window.crypto && typeof crypto.randomUUID === 'function') ? crypto.randomUUID() : 'ac-' + Date.now() + '-' + Math.random().toString(36).slice(2)
3372
- return remoteFace.session.prompt({ requestId: reqId, sessionId: newId, mode: 'queue', content: [{ type: 'text', text: d.carryText }] })
3373
- })
3374
- })
3375
- }).then(function () {
3376
- try { localStorage.setItem('dsh-auto-memory.autoCont.last', String(Date.now())) } catch (eL) {}
3377
- setAcSt(locale === 'zh' ? '✓ 已自动接续到新会话,可直接上手' : '✓ Auto-continued into a new session')
3588
+ setAcSt(locale === 'zh' ? '正在自动接续:刷新白板/账本 → 建新会话 → 注入材料…' : 'Auto-continuing: refresh PLAN/ledger → new session → inject…')
3589
+ runContinueFlow({ onProgress: function (m) { setAcSt(m) } }).then(function (r) {
3590
+ setAcSt((locale === 'zh' ? '✓ 已自动接续到新会话,可直接上手' : '✓ Auto-continued into a new session') + (r && r.model ? ' · ' + r.model + (r.reasoningEffort ? '/' + r.reasoningEffort : '') : ''))
3378
3591
  setAcBusy(false)
3379
3592
  }).catch(function (e) { setAcSt('✗ ' + String(e && e.message ? e.message : e)); setAcBusy(false) })
3380
3593
  }
3381
- if (acCd > 0 || acSt) {
3594
+ var onAgree = function () { runAuto() }
3595
+ var onReject = function () {
3596
+ var edgeAt = (acConfirm && acConfirm.edgeAt) || autoState.edgeAt || 0
3597
+ cancelAc()
3598
+ setAcConfirm(null)
3599
+ autoState.rejectedEdgeAt = edgeAt
3600
+ setAcSt(t('autoContRejected'))
3601
+ }
3602
+ if (acConfirm || acCd > 0 || acSt) {
3382
3603
  return h('div', { 'data-dam-autocont': '', style: { position: 'fixed', right: '16px', bottom: '16px', zIndex: 9000, maxWidth: 'min(440px, calc(100vw - 32px))', boxSizing: 'border-box', padding: '10px 12px', borderRadius: '12px', border: '1px solid rgba(128,128,128,.35)', background: 'rgba(18,22,30,.93)', color: '#e8eaed', fontSize: '12px', lineHeight: '1.55', fontFamily: 'system-ui', boxShadow: '0 10px 30px rgba(0,0,0,.35)', backdropFilter: 'blur(10px)' } },
3383
- acCd > 0 ? h('div', null, t('autoContCountdown').replace('{s}', String(acCd)), ' ', h('button', { style: { marginLeft: '6px', border: '1px solid rgba(232,234,237,.45)', borderRadius: '8px', background: 'transparent', color: '#e8eaed', cursor: 'pointer', padding: '2px 10px' }, onClick: cancelAc }, t('autoContCancel'))) : null,
3384
- acSt ? h('div', { style: acCd > 0 ? { marginTop: '4px' } : {} }, acSt) : null)
3604
+ acConfirm ? h('div', { 'data-dam-autocont-confirm': '' }, [
3605
+ h('div', { style: { marginBottom: '4px' } }, t('autoContConfirm').replace('{p}', String(Math.round((acConfirm.ratio || 0) * 100)))),
3606
+ h('div', { 'data-dam-hint': '' }, (acConfirm.tokens || 0).toLocaleString() + ' / ' + (acConfirm.window || 0).toLocaleString() + ' token'),
3607
+ h('div', { style: { marginTop: '8px' } },
3608
+ h('button', { 'data-dam-btn': '', onClick: onAgree }, t('autoContAgree')),
3609
+ h('button', { 'data-dam-btn': '', style: { marginLeft: '6px' }, onClick: onReject }, t('autoContReject')),
3610
+ acCd > 0 ? h('span', { 'data-dam-hint': '', style: { marginLeft: '8px' } }, t('autoContTimeout').replace('{s}', String(acCd))) : null),
3611
+ ]) : null,
3612
+ !acConfirm && acCd > 0 ? h('div', null, t('autoContCountdown').replace('{s}', String(acCd)), ' ', h('button', { style: { marginLeft: '6px', border: '1px solid rgba(232,234,237,.45)', borderRadius: '8px', background: 'transparent', color: '#e8eaed', cursor: 'pointer', padding: '2px 10px' }, onClick: cancelAc }, t('autoContCancel'))) : null,
3613
+ acSt ? h('div', { style: (acConfirm || acCd > 0) ? { marginTop: '4px' } : {} }, acSt) : null)
3385
3614
  }
3386
3615
  return null
3387
3616
  }
@@ -3910,9 +4139,13 @@ window.__ModuleLoader__.load({
3910
4139
  field(t('fHandoffPlan'), h('input', { 'data-dam-input': '', type: 'number', min: 200, value: cfg.handoffPlanChars === undefined ? 1200 : cfg.handoffPlanChars, onChange: function (e) { set('handoffPlanChars', Number(e.target.value) || 1200) } }), t('fHandoffPlanHint')),
3911
4140
  field(t('fHandoffLedger'), h('input', { 'data-dam-input': '', type: 'number', min: 100, value: cfg.handoffLedgerChars === undefined ? 800 : cfg.handoffLedgerChars, onChange: function (e) { set('handoffLedgerChars', Number(e.target.value) || 800) } }), t('fHandoffLedgerHint')),
3912
4141
  field(t('fWaterWindow'), h('input', { 'data-dam-input': '', type: 'number', min: 0, value: cfg.waterLevelWindowTokens === undefined ? 65536 : cfg.waterLevelWindowTokens, onChange: function (e) { set('waterLevelWindowTokens', Number(e.target.value) || 0) } }), t('fWaterWindowHint')),
3913
- field(t('fWaterThreshold'), h('input', { 'data-dam-input': '', type: 'number', min: 0.1, max: 1.5, step: 0.05, value: cfg.waterLevelThreshold === undefined ? 0.8 : cfg.waterLevelThreshold, onChange: function (e) { set('waterLevelThreshold', Number(e.target.value) || 0.8) } }), t('fWaterThresholdHint')),
4142
+ field(t('fWaterThreshold'), h('input', { 'data-dam-input': '', type: 'number', min: 0.1, max: 1.5, step: 0.05, value: cfg.waterLevelThreshold === undefined ? 0.75 : cfg.waterLevelThreshold, onChange: function (e) { set('waterLevelThreshold', Number(e.target.value) || 0.75) } }), t('fWaterThresholdHint')),
3914
4143
  field(t('fWaterAdvisory'), h('input', { type: 'checkbox', checked: cfg.waterLevelAdvisory !== false, onChange: function (e) { set('waterLevelAdvisory', e.target.checked) } }), t('fWaterAdvisoryHint')),
3915
- field(t('fWaterAuto'), h('input', { type: 'checkbox', checked: cfg.waterLevelAutoHandoff !== false, onChange: function (e) { set('waterLevelAutoHandoff', e.target.checked) } }), t('fWaterAutoHint'))]),
4144
+ field(t('fWaterAuto'), h('input', { type: 'checkbox', checked: cfg.waterLevelAutoHandoff !== false, onChange: function (e) { set('waterLevelAutoHandoff', e.target.checked) } }), t('fWaterAutoHint')),
4145
+ field(locale === 'zh' ? '子代理痕迹回收' : 'Subagent trace recycle', h('input', { type: 'checkbox', checked: cfg.subagentGcEnabled !== false, onChange: function (e) { set('subagentGcEnabled', e.target.checked) } }),
4146
+ 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.'),
4147
+ 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) } }),
4148
+ 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.')]),
3916
4149
  section('maintenance', sectionLabels.maintenance, [field(t('fVersion'), h('div', { 'data-dam-row': '' },
3917
4150
  h('span', { style: { flex: 1 } }, verInfo ? (verInfo.current || '?') + (verInfo.latest ? ' → ' + verInfo.latest + (verInfo.upToDate ? ' ' + t('upToDate') : ' ' + t('hasUpdate')) : '') : (checkingUpdate ? t('checking') : '—')),
3918
4151
  h('button', { 'data-dam-btn': '', onClick: checkUpdate, disabled: checkingUpdate }, checkingUpdate ? t('checking') : t('checkUpdate')),
@@ -3951,6 +4184,7 @@ window.__ModuleLoader__.load({
3951
4184
  try {
3952
4185
  var onDocPointerDown = function (e) {
3953
4186
  if (!panelOpen || panelClosing) return
4187
+ if (controller.isPinned()) return // 悬浮钉:钉住后点面板外不收起(2026-09-08)
3954
4188
  var el = e.target
3955
4189
  if (el && el.closest && (el.closest('[data-dam-panel]') || el.closest('[data-dam-sidebar-btn]'))) return
3956
4190
  controller.close()
@@ -4172,7 +4406,7 @@ window.__ModuleLoader__.load({
4172
4406
  console.log('[dsh-auto-memory] client ready: sidebar entry + panel + settings page')
4173
4407
  }
4174
4408
 
4175
- exports.inject = ['slots', 'sessions']
4409
+ exports.inject = ['slots', 'sessions', 'remote', 'remote.session']
4176
4410
  exports.apply = apply
4177
4411
  return module.exports
4178
4412
  },