@a9i5k4/dsh-auto-memory 2.2.2 → 2.2.4

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 (113) hide show
  1. package/docs/A3-RISK-ASSESSMENT-20260830.md +61 -0
  2. package/docs/COT-WATCH-RFC.md +88 -0
  3. package/docs/DUAL-TIER-RATIFICATION-PROMPT.md +71 -0
  4. package/docs/HANDOFF-HARNESS.md +78 -0
  5. package/docs/HANDOFF-M8-M9-M10.md +203 -0
  6. package/docs/HY4-TOUR-LOGO-HANDOFF-2.md +60 -0
  7. package/docs/HY4-TOUR-LOGO-HANDOFF.md +100 -0
  8. package/docs/ISSUE-REPLY-UNATTENDED.md +31 -0
  9. package/docs/K3-LANDING-HANDOFF.md +76 -0
  10. package/docs/LANDING-OUTLINE.md +85 -0
  11. package/docs/M-CM-PLAN.md +166 -0
  12. package/docs/M-CM-STATE.md +64 -0
  13. package/docs/M3B-CONTRACT.md +461 -0
  14. package/docs/M4-CONTRACT.md +1207 -0
  15. package/docs/M5-CONTRACT.md +383 -0
  16. package/docs/M6-CONTRACT.md +342 -0
  17. package/docs/M7-ACTIVATION-ALGO-REFERENCES.md +104 -0
  18. package/docs/M7-ACTIVATION-CALIBRATION.md +144 -0
  19. package/docs/M7-ACTIVATION-FEATURE-AGENT-PROMPT.md +79 -0
  20. package/docs/M7-ACTIVATION-FEATURE-CALIBRATION.md +58 -0
  21. package/docs/M7-ACTIVATION-FEATURE-DESIGN.md +124 -0
  22. package/docs/M7-ACTIVATION-V2-CONTROLLED-SHADOW.md +127 -0
  23. package/docs/M7-ACTIVATION-V2-HANDOFF.md +132 -0
  24. package/docs/M7-ACTIVATION-V2-HOLDEDOUT-EVAL.md +94 -0
  25. package/docs/M7-ACTIVATION-V2-HOLDEDOUT-SHADOW.md +60 -0
  26. package/docs/M7-ACTIVATION-V2-LIVE-SHADOW-PLAN.md +85 -0
  27. package/docs/M7-ACTIVATION-V2-PAPER.md +303 -0
  28. package/docs/M7-AGENT-HANDOFF-PROMPT.md +63 -0
  29. package/docs/M7-ALGORITHM-DECISION.md +174 -0
  30. package/docs/M7-AUTONOMOUS-STATE.md +252 -0
  31. package/docs/M7-BENCHMARK-PLAN.md +78 -0
  32. package/docs/M7-CLOSED-LOOP-WIRING.md +110 -0
  33. package/docs/M7-EMBEDDING-BENCHMARK.md +164 -0
  34. package/docs/M7-INTERFACE-DIGEST.md +144 -0
  35. package/docs/M7-LABEL-REVIEW-REPORT.md +107 -0
  36. package/docs/M7-LEXICAL-TUNING.md +58 -0
  37. package/docs/M7-LIVE-SHADOW-SCRIPT.md +30 -0
  38. package/docs/M7-PYTHON-IMPLEMENTATION-REPORT.md +92 -0
  39. package/docs/M7-RESEARCH-PAPER.md +442 -0
  40. package/docs/M7-TASKSET-DISPATCH.md +213 -0
  41. package/docs/M8-MEMORY-HUB.md +105 -0
  42. package/docs/MEMORY-SYSTEMS-SURVEY-2026-09.md +166 -0
  43. package/docs/NEXT-MAJOR-PROMO.md +363 -0
  44. package/docs/NEXT-MAJOR-README-DRAFT.zh.md +205 -0
  45. package/docs/NEXT-MAJOR-VISION.md +112 -0
  46. package/docs/PREVIEW-NEXT-STEPS.md +229 -0
  47. package/docs/PROJECT-FREEZE-AND-ROADMAP.md +200 -0
  48. package/docs/PROMO-STYLE-GUIDE.md +84 -0
  49. package/docs/PYTHON-SIDECAR-CONTRACT.md +539 -0
  50. package/docs/R2-POLICY-PLUMBING-BLUEPRINT.md +99 -0
  51. package/docs/RELEASE-READINESS-PLAN.md +91 -0
  52. package/docs/RELEASE-SEMANTIC-OPTION.md +181 -0
  53. package/docs/S1-SCIENTIFIC-RIGOR.md +269 -0
  54. package/docs/S2-DEEP-ABSORPTION.md +259 -0
  55. package/docs/S3-TARGET-ARCHITECTURE.md +172 -0
  56. package/docs/USER-GUIDE.zh-CN.md +283 -0
  57. package/docs/banner.jpg +0 -0
  58. package/docs/implementation-handoff-context.zh-CN.md +429 -0
  59. package/docs/landing/index.html +1745 -0
  60. package/docs/paper-figures/fig1_model_quality.png +0 -0
  61. package/docs/paper-figures/fig2_chunk_reversal.png +0 -0
  62. package/docs/paper-figures/fig3_latency.png +0 -0
  63. package/docs/paper-figures/fig4_hybrid.png +0 -0
  64. package/docs/paper-figures/fig5_rerank_tradeoff.png +0 -0
  65. package/docs/paper-figures/fig6_cluster_sweep.png +0 -0
  66. package/docs/paper-figures/fig7_resource.png +0 -0
  67. package/docs/paper-figures-v2/fig1_echo_trap.png +0 -0
  68. package/docs/paper-figures-v2/fig2_pr_paths.png +0 -0
  69. package/docs/paper-figures-v2/fig3_coefficients.png +0 -0
  70. package/docs/paper-figures-v2/fig4_calibration.png +0 -0
  71. package/docs/paper-figures-v2/fig5_order_ablation.png +0 -0
  72. package/docs/paper-figures-v2/fig6_containment.png +0 -0
  73. package/docs/proactive-associative-memory-architecture.html +659 -0
  74. package/docs/proactive-associative-memory-meta-code.html +1058 -0
  75. package/docs/proactive-associative-memory-research-report.zh-CN.md +685 -0
  76. package/docs/proactive-associative-memory-system-map.html +1580 -0
  77. package/docs/promo/first-run-guide.html +397 -0
  78. package/docs/promo/homepage.html +384 -0
  79. package/docs/screenshots/calendar-en.png +0 -0
  80. package/docs/screenshots/calendar-zh.png +0 -0
  81. package/docs/screenshots/connect-en.png +0 -0
  82. package/docs/screenshots/connect-zh.png +0 -0
  83. package/docs/screenshots/main-connect-en.png +0 -0
  84. package/docs/screenshots/main-connect-zh.png +0 -0
  85. package/docs/screenshots/overview-en.png +0 -0
  86. package/docs/screenshots/overview-zh.png +0 -0
  87. package/docs/screenshots/panel-hub.png +0 -0
  88. package/docs/screenshots/panel-overview.png +0 -0
  89. package/docs/screenshots/panel-refine.png +0 -0
  90. package/docs/screenshots/promo/promo-0-banner-v2.png +0 -0
  91. package/docs/screenshots/promo/promo-1-hero.png +0 -0
  92. package/docs/screenshots/promo/promo-2-tour.png +0 -0
  93. package/docs/screenshots/promo/promo-3-recall.png +0 -0
  94. package/docs/screenshots/promo/promo-4-unattended.png +0 -0
  95. package/docs/screenshots/promo/promo-5-external.png +0 -0
  96. package/docs/screenshots/promo/promo-6-greeting.png +0 -0
  97. package/docs/screenshots/reflections-en.png +0 -0
  98. package/docs/screenshots/search-zh.png +0 -0
  99. package/docs/screenshots/settings-2-zh.png +0 -0
  100. package/docs/screenshots/settings-debug-zh.png +0 -0
  101. package/docs/screenshots/settings-en.png +0 -0
  102. package/docs/screenshots/settings-zh.png +0 -0
  103. package/docs/screenshots/tour-core.png +0 -0
  104. package/docs/screenshots/tour-external.png +0 -0
  105. package/docs/screenshots/tour-toggles.png +0 -0
  106. package/docs/screenshots/tour-welcome.png +0 -0
  107. package/docs/screenshots/workspace-map-zh.png +0 -0
  108. package/docs/social-preview.png +0 -0
  109. package/lib/client.js +277 -118
  110. package/lib/index.js +329 -60
  111. package/lib/subagent-gc.js +239 -0
  112. package/lib/water-window.js +88 -0
  113. package/package.json +2 -1
package/lib/client.js CHANGED
@@ -270,8 +270,8 @@ window.__ModuleLoader__.load({
270
270
  fConsScheduleDays: '固化回看天数', fConsScheduleDaysHint: '定时固化读取最近 N 天日志;默认 7。',
271
271
  fMaintSchedule: '定时 30 天蒸馏', fMaintScheduleHint: '每天到点自动把超过 30 天的旧日志蒸馏归档(等价 memory_maintain);无旧日志时零成本跳过。',
272
272
  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: '取消',
273
+ waterCardTitle: '上下文水位', waterAutoSrc: '自动检测', waterManualSrc: '手动设定', waterOfficialSrc: '官方路由容量', waterFallbackSrc: '回退默认值', waterMeterOfficial: '官方计量', waterMeterHeuristic: '启发式降级', waterThresholdHint: '水位达到 {t} 时注入交接建议并自动补写账本;计量优先用官方 token-meter(与聊天框 context ring 同源),不可用时降级启发式估算;窗口优先取官方路由容量(request/context 的 contextWindow),其次按当前模型查 settings.yaml contextWindow,也可在下方手动覆盖。',
274
+ autoContTitle: '自动接续', autoContHint: '开启后:水位达到阈值、且旧会话已空闲(harness 权威 running 位在轮次边界转为 false,取代旧「两轮水位持平」)时,弹出确认卡——同意=立即接续,拒绝=本轮跳过(同一边界不再提示),30-40 秒无操作视为挂机自动接续;接续前先让旧 Agent 刷新白板 PLAN 与交接账本,再建新会话(沿用工作区/模型/思考档位)并注入分层交接材料;触发后默认 30 分钟内不重复。', autoContOn: '自动接续:开', autoContOff: '自动接续:关', autoContThreshold: '阈值', autoContCountdown: '⏳ {s} 秒后自动接续…', autoContCancel: '取消', autoContConfirm: '本会话水位已达 {p}%,旧会话已空闲(轮次边界)——现在接续到新会话?', autoContAgree: '同意接续', autoContReject: '拒绝', autoContTimeout: '{s} 秒后自动接续(视为挂机)', autoContRejected: '已跳过本次接续(下一个轮次边界再提醒)',
275
275
  continueCardTitle: '一键接续', continueCardHint: '写好交接账本→创建新会话→把交接材料作为首条消息预载,并自动切换到新会话继续任务(接续材料含白板节选+最新账本;当前工作区无账本时自动取全局最近一篇)。', continueBtn: '一键接续到新会话', continueBusy: '处理中…',
276
276
  fStyle: '反思风格', fStyleHint: '生活化 / 专业性 / 由内容决定。',
277
277
  fLocale: '界面语言', fLocaleHint: '默认跟随 DSH 系统语言;也可手动指定中文 / English。', followSystem: '跟随系统语言',
@@ -418,8 +418,8 @@ window.__ModuleLoader__.load({
418
418
  fConsScheduleDays: 'Consolidation lookback (days)', fConsScheduleDaysHint: 'How many recent days of logs the scheduled consolidation reads; default 7.',
419
419
  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
420
  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',
421
+ waterCardTitle: 'Context water level', waterAutoSrc: 'auto-detected', waterManualSrc: 'manual', waterOfficialSrc: 'route capacity', waterFallbackSrc: 'fallback default', waterMeterOfficial: 'official meter', waterMeterHeuristic: 'heuristic fallback', waterThresholdHint: 'Past {t} of the window, the handoff advisory is injected and the skeleton ledger is backfilled; metering prefers the official token-meter (same source as the chat context ring), falling back to the heuristic when unavailable; the window prefers the official route capacity (request/context contextWindow), then the active model in settings.yaml, or your manual override below.',
422
+ 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
423
  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
424
  fStyle: 'Reflection style', fStyleHint: 'Life-style / Professional / Auto.',
425
425
  fLocale: 'UI language', fLocaleHint: 'Follows the DSH system language by default; you can also pin Chinese / English.', followSystem: 'Follow system language',
@@ -446,6 +446,21 @@ window.__ModuleLoader__.load({
446
446
  }
447
447
  var sessions = null
448
448
  var remoteFace = null
449
+ // 官方 session.create 返回形态多样(裸字符串 / {sessionId} / {id} / {value} / {ok,value:{sessionId}}),统一提取会话 id
450
+ function extractSessionId(created) {
451
+ if (created == null) return null
452
+ if (typeof created === 'string') return created
453
+ if (typeof created !== 'object') return null
454
+ if (typeof created.sessionId === 'string') return created.sessionId
455
+ if (typeof created.id === 'string') return created.id
456
+ var v = created.ok && created.value !== undefined ? created.value : (created.value !== undefined ? created.value : null)
457
+ if (v && typeof v === 'string') return v
458
+ if (v && typeof v === 'object') {
459
+ if (typeof v.sessionId === 'string') return v.sessionId
460
+ if (typeof v.id === 'string') return v.id
461
+ }
462
+ return null
463
+ }
449
464
  // 当前会话工作区(跟随 GUI 切换工作区):从 sessions 服务拿,供 state/summarize/greet 请求使用
450
465
  function currentWs() {
451
466
  try {
@@ -457,6 +472,32 @@ window.__ModuleLoader__.load({
457
472
  return (s && typeof s.cwd === 'string') ? s.cwd : ''
458
473
  } catch (e) { return '' }
459
474
  }
475
+ // 轮次边界探测(①,2026-09-08 2.2.4):读 harness 权威 running 位(sessions.list 快照,官方 buildListSnapshot 投影),
476
+ // 取代旧「连续两轮水位不增长=空闲」——长工具调用会被误判空闲、真挂机又永不触发。
477
+ function currentRunningInfo() {
478
+ try {
479
+ var snap = sessions && sessions.list && sessions.list.getSnapshot()
480
+ if (!snap) return null
481
+ var id = snap.current
482
+ if (id === undefined || id === null) return null
483
+ var s = snap.byId && snap.byId[id]
484
+ if (!s) return null
485
+ return { sessionId: id, running: !!s.running }
486
+ } catch (e) { return null }
487
+ }
488
+ function runningOfSession(sessionId) {
489
+ try {
490
+ var snap = sessions && sessions.list && sessions.list.getSnapshot()
491
+ var s = snap && snap.byId ? snap.byId[sessionId] : null
492
+ return s ? !!s.running : null
493
+ } catch (e) { return null }
494
+ }
495
+ function amCtzValue() {
496
+ return (typeof Intl !== 'undefined' && typeof Intl.DateTimeFormat === 'function') ? (Intl.DateTimeFormat().resolvedOptions().timeZone || undefined) : undefined
497
+ }
498
+ function amReqId(prefix) {
499
+ return (window.crypto && typeof crypto.randomUUID === 'function') ? crypto.randomUUID() : (prefix || 'amc') + '-' + Date.now() + '-' + Math.random().toString(36).slice(2)
500
+ }
460
501
  var fontScale = 'lg'
461
502
  var FONT_SCALES = { sm: '小', md: '标准', lg: '大', xl: '特大' }
462
503
  // 与服务端 lib/index.js DEFAULT_PROMPT_LAYERS 保持一致(设置页显示各层默认文案)
@@ -501,6 +542,27 @@ window.__ModuleLoader__.load({
501
542
 
502
543
  // ───────────────────────── 更新弹窗 / 首次指导 ─────────────────────────
503
544
  var CHANGELOG = {
545
+ '2.2.4': { zh: [
546
+ '★ 修复:一键接续的新会话终于落进旧会话所属工作区——官方 session.create 只有传 workspaceId 才会绑定工作区,只传 cwd 仅设工作目录(故此前恒落「未分组」);现 host 先解析旧会话的 workspaceId 再建会话,解析不到才回退 cwd。',
547
+ '★ 修复:新会话沿用旧会话的模型与思考档位——模型信息在会话日志的 request/header.header.config(旧版查错了事件类型,恒取空 → 回落默认模型)。',
548
+ '★ 自动接续 v3:触发条件改为「水位≥阈值 且 harness 权威 running==false(轮次边界)」,弃用「两轮水位持平」——长工具调用不再误触发,真挂机一定触发;达阈值先弹确认卡:同意=立即接续 / 拒绝=本轮跳过(同一边界不再提示)/ 30-40 秒无操作=视为挂机自动接续。',
549
+ '★ 接续前刷新仪式:确认后先让旧 Agent 把白板 PLAN 与交接账本刷到最新,再用最新材料组装交接(可在配置里关掉);新窗口不再拿到过时白板。',
550
+ '★ 修复:上下文水位虚高(显示「500,998 / 131,072 token · 150%」)——模型窗口解析两条路同时失效:① settings.yaml 的 llm-deepseek 段是 flow 风格 YAML,旧解析器只认 block 风格;② 官方 request/context 的 contextWindow 只在会话开头出现(实测 2999 条事件里仅 2 条),旧代码只扫最近 256 条 → 1M 窗口被当成 128K。现在优先取官方路由容量、全量扫描并按会话缓存,设置页如实标出「官方路由容量 / 回退默认值」,百分比也不再截断到 150%。',
551
+ '★ 接续材料分层压缩:第0层指令+白板 / 第1层交接账本 / 第2层近期线程(最近 20 条 × 700 字,保留角色与工具标记)/ 第3层完整转写按需 read;材料带时间戳与「白板比账本旧」过期提示。',
552
+ '★ 新增子代理痕迹回收:自动沉淀 / 时段总结 / 问候 / 蒸馏等一次性子代理每跑一次都会在 ~/.dsh/sessions 下留下一个持久化会话(实测全机 686 个子代理会话里 638 个来自本插件),积累上千后会拖慢会话列表;现在子代理一结束就把痕迹移入 ~/.dsh/subagent-gc-backup(只移动不删除,可回滚),另有每日兜底巡检,设置页可开关并调整保留天数。',
553
+ ], en: [
554
+ '★ 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.',
555
+ '★ 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).',
556
+ '★ 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.',
557
+ '★ Refresh ritual before continuing: the old agent refreshes PLAN + ledger first so the new session never starts from a stale whiteboard (configurable).',
558
+ '★ 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.',
559
+ '★ 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.',
560
+ ] },
561
+ '2.2.3': { zh: [
562
+ '打包修复:npm 包 files 现包含 docs/,用户文档随包发布——README 的「📖 用户文档」链接在 GitHub 与 npm 包页均可打开。',
563
+ ], en: [
564
+ 'Packaging: the npm package now ships docs/ via the files field, so the README user-guide link works on both GitHub and the npm package page.',
565
+ ] },
504
566
  '2.2.2': { zh: [
505
567
  '修复:一键接续在部分环境报「harness 未提供 remote.session」——补上官方 remote 注入面(@deepseek-ai/dsh-api-remotes),重启后自动建会话/切换/注入恢复可用。',
506
568
  '修复:语义引擎检测面板「未就绪」与「一切就绪」同屏矛盾、缺失时不弹安装引导——检测面补 Python int8 探测(无装=未就绪),按当前引擎模式自动弹出对应安装引导卡。',
@@ -875,7 +937,7 @@ window.__ModuleLoader__.load({
875
937
  '[data-dam-banner] { border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #e6a23c) 55%, transparent);',
876
938
  ' 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)); }',
877
939
  '[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; }',
878
- '[data-dam-row] { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }',
940
+ '[data-dam-row] { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; flex-wrap: wrap; }',
879
941
  '[data-dam-row] label { flex: 0 0 110px; opacity: .8; font-size: calc(12.5px * var(--dam-scale)); }',
880
942
  '[data-dam-hint] { opacity: .5; font-size: calc(11.5px * var(--dam-scale)); margin-top: 2px; }',
881
943
  '[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; }',
@@ -1850,7 +1912,101 @@ window.__ModuleLoader__.load({
1850
1912
  }
1851
1913
 
1852
1914
  // M-CM6-C 自动接续跨渲染状态(apply 作用域,插件生命周期存活;组件重挂不重置)
1853
- var autoState = { cdIv: null, lastTokens: null }
1915
+ // v3(2.2.4):lastRunning/edgeAt=轮次边界;rejectedEdgeAt=拒绝后同一边界不重复提示;suppressUntil=接续进行中不重复触发
1916
+ var autoState = { cdIv: null, lastTokens: null, lastRunning: null, edgeAt: 0, edgeSession: '', rejectedEdgeAt: 0, promptVisible: false, suppressUntil: 0 }
1917
+
1918
+ // ───────── 接续执行器(一键接续 + 自动接续共用,2026-09-08 2.2.4)─────────
1919
+ // 流程:③刷新仪式(旧 Agent 先刷 PLAN+账本)→ 取材料 → 修A(workspaceId 建会话)→ 序号标题
1920
+ // → 修B(沿用模型+思考档位)→ 打开 → 注入材料。
1921
+ var continueFlowBusy = false
1922
+ function continueCreateArgs(d) {
1923
+ // 修A(2026-09-08):官方 session.create 只接受 workspaceId 或 cwd 之一(同传报 bad-request),
1924
+ // 且只有 workspaceId 分支会 workspace.attachSession()(api-session-controller/lib/index.js:566-588);
1925
+ // 只传 cwd 仅设工作目录 → 新会话永远落「未分组工作区」。故优先 workspaceId,解析不到才回退 cwd。
1926
+ if (d && d.workspaceId) return { workspaceId: d.workspaceId, agentPreset: d.agentPreset || undefined }
1927
+ return d && d.ws ? { cwd: d.ws, agentPreset: d.agentPreset || undefined } : { agentPreset: (d && d.agentPreset) || undefined }
1928
+ }
1929
+ async function refreshOldSession(onMsg) {
1930
+ // ③接续前刷新仪式:让旧 Agent 把 PLAN.md 与交接账本刷到最新,host 再用最新材料组装交接。
1931
+ // 任何失败/超时都 fail-soft(用现有材料继续),绝不阻塞接续。
1932
+ try {
1933
+ var st = await apiGet(API.handoffState)
1934
+ var rf = st && st.refresh
1935
+ if (!rf || !rf.sessionId || !rf.prompt) return { ok: false, skipped: true }
1936
+ if (!remoteFace || !remoteFace.session || typeof remoteFace.session.prompt !== 'function') return { ok: false, skipped: true }
1937
+ var before = String((st.planMtime || 0)) + '|' + String((st.ledgers && st.ledgers[0] && st.ledgers[0].name) || '')
1938
+ if (onMsg) onMsg(locale === 'zh' ? '刷新仪式:请旧 Agent 更新白板 PLAN 与交接账本…' : 'Refresh ritual: asking the old agent to update PLAN + ledger…')
1939
+ await remoteFace.session.prompt({ requestId: amReqId('amr'), sessionId: rf.sessionId, mode: 'queue', content: [{ type: 'text', text: rf.prompt }], clientTimeZone: amCtzValue() })
1940
+ var waited = await waitForRefresh(rf.sessionId, before)
1941
+ return { ok: true, waited: waited }
1942
+ } catch (eRf) { return { ok: false, error: String(eRf && eRf.message ? eRf.message : eRf) } }
1943
+ }
1944
+ async function waitForRefresh(oldId, before) {
1945
+ // 结束条件(任一):材料变了 / 旧会话跑完这一轮(true→false) / 超时(默认 90s,配置可调)。
1946
+ var t0 = Date.now()
1947
+ var timeoutMs = 90000
1948
+ try {
1949
+ var cfg = await apiGet(API.config)
1950
+ var sec = cfg && Number(cfg.autoContinueRefreshTimeoutSeconds)
1951
+ if (sec >= 15 && sec <= 600) timeoutMs = sec * 1000
1952
+ } catch (eC) {}
1953
+ var sawRunning = false
1954
+ while (Date.now() - t0 < timeoutMs) {
1955
+ await new Promise(function (r) { setTimeout(r, 2500) })
1956
+ var run = runningOfSession(oldId)
1957
+ if (run === true) sawRunning = true
1958
+ var st2 = null
1959
+ try { st2 = await apiGet(API.handoffState) } catch (eG) {}
1960
+ var now = String((st2 && st2.planMtime) || 0) + '|' + String((st2 && st2.ledgers && st2.ledgers[0] && st2.ledgers[0].name) || '')
1961
+ if (now !== before) return 'updated'
1962
+ if (sawRunning && run === false) return 'turn-ended'
1963
+ }
1964
+ return 'timeout'
1965
+ }
1966
+ async function executeContinue(d, onMsg) {
1967
+ var rf = remoteFace
1968
+ if (!rf || !rf.session || typeof rf.session.create !== 'function') throw new Error(locale === 'zh' ? 'harness 未提供 remote.session(版本过旧?)' : 'remote.session unavailable (harness too old?)')
1969
+ if (onMsg) onMsg(locale === 'zh' ? '正在创建新会话(沿用旧工作区与模型)…' : 'Creating session (same workspace & model)…')
1970
+ var created
1971
+ try { created = await rf.session.create(continueCreateArgs(d)) } catch (eCreate) {
1972
+ // workspaceId 已失效(工作区被删)或旧 harness 不认 → 回退 cwd,再退纯 agentPreset
1973
+ created = await rf.session.create(d && d.ws ? { cwd: d.ws, agentPreset: d.agentPreset || undefined } : { agentPreset: (d && d.agentPreset) || undefined })
1974
+ }
1975
+ // 官方 create 返回裸 SessionId 字符串(而非对象),兼容字符串/{sessionId}/{id}/{value}/{ok,value}
1976
+ var newId = extractSessionId(created)
1977
+ if (!newId) throw new Error('create: no sessionId')
1978
+ // 接续序号标题:让用户一眼区分新会话 vs 延续会话
1979
+ if (d.contSeq && rf.session.rename && typeof rf.session.rename === 'function') {
1980
+ var contTitle = (locale === 'zh' ? '接续 #' : 'Cont.#') + String(d.contSeq) + (d.wsBase ? ' · ' + d.wsBase : '')
1981
+ try { await rf.session.rename({ sessionId: newId, title: contTitle }) } catch (eRen) {}
1982
+ }
1983
+ // 沿用旧会话模型(provider/model/reasoningEffort 由 host 从旧会话 request/header 折叠而来;不可用则静默回退路由默认)
1984
+ if (d.provider && d.model && typeof rf.session.selectModel === 'function') {
1985
+ if (onMsg) onMsg(locale === 'zh' ? '正在沿用旧模型 ' + d.model + (d.reasoningEffort ? '(' + d.reasoningEffort + ')' : '') + '…' : 'Restoring model ' + d.model + '…')
1986
+ try { await rf.session.selectModel({ sessionId: newId, provider: d.provider, model: d.model, reasoningEffort: d.reasoningEffort || undefined }) } catch (eMdl) {}
1987
+ }
1988
+ try { if (sessions && typeof sessions.open === 'function') sessions.open(newId) } catch (eOpen) {}
1989
+ await rf.session.prompt({ requestId: amReqId('amc'), sessionId: newId, mode: 'queue', content: [{ type: 'text', text: d.carryText }], clientTimeZone: amCtzValue() })
1990
+ return newId
1991
+ }
1992
+ async function runContinueFlow(opts) {
1993
+ var onMsg = (opts && opts.onProgress) || function () {}
1994
+ if (continueFlowBusy) return { ok: false, error: 'busy' }
1995
+ continueFlowBusy = true
1996
+ autoState.suppressUntil = Date.now() + 5 * 60 * 1000
1997
+ try {
1998
+ var cfg = null
1999
+ try { cfg = await apiGet(API.config) } catch (eCfg) {}
2000
+ var ritual = !(cfg && cfg.autoContinueRefreshRitual === false) && !(opts && opts.skipRefresh)
2001
+ if (ritual) { try { await refreshOldSession(onMsg) } catch (eRf) {} }
2002
+ onMsg(locale === 'zh' ? '正在构造交接材料(含旧会话转写)…' : 'Preparing handoff material (incl. prev-session transcript)…')
2003
+ var d = await apiPost(API.handoffContinue, {})
2004
+ if (!d || !d.ok) throw new Error((d && d.error) || 'no handoff material')
2005
+ var newId = await executeContinue(d, onMsg)
2006
+ try { localStorage.setItem('dsh-auto-memory.autoCont.last', String(Date.now())) } catch (eL) {}
2007
+ return { ok: true, sessionId: newId, workspaceId: d.workspaceId || '', model: d.model || '', reasoningEffort: d.reasoningEffort || '' }
2008
+ } finally { continueFlowBusy = false }
2009
+ }
1854
2010
 
1855
2011
  function PlanTab() {
1856
2012
  var dataPair = useState(null)
@@ -1869,13 +2025,10 @@ window.__ModuleLoader__.load({
1869
2025
  var contMsgPair = useState('')
1870
2026
  var contMsg = contMsgPair[0]
1871
2027
  var setContMsg = contMsgPair[1]
1872
- // M-CM6-C 自动接续(水位阈值触发,默认关;轮询 handoff-state,连续两轮水位不增长视为会话空闲)
2028
+ // M-CM6-C 自动接续(①轮次边界触发 + ②确认卡由全局 AutoContinueHost 执行;这里只读/写开关与阈值)
1873
2029
  var autoPair = useState(null)
1874
2030
  var autoCfg = autoPair[0]
1875
2031
  var setAutoCfg = autoPair[1]
1876
- var cdPair = useState(0)
1877
- var cd = cdPair[0]
1878
- var setCd = cdPair[1]
1879
2032
  useEffect(function () {
1880
2033
  apiGet(API.config).then(function (c) {
1881
2034
  setAutoCfg({ enabled: !(c && c.autoContinueEnabled === false), threshold: (c && Number(c.autoContinueThreshold)) || 0.8 })
@@ -1885,47 +2038,13 @@ window.__ModuleLoader__.load({
1885
2038
  setAutoCfg(function (p) { return Object.assign({}, p, patch) })
1886
2039
  apiPost(API.config, patch).catch(function () {})
1887
2040
  }
1888
- var autoCancel = function () {
1889
- if (autoState.cdIv) { clearInterval(autoState.cdIv); autoState.cdIv = null }
1890
- setCd(0)
1891
- }
1892
- var autoStartCountdown = function () {
1893
- if (autoState.cdIv) return
1894
- var left = 30
1895
- setCd(left)
1896
- autoState.cdIv = setInterval(function () {
1897
- left -= 1
1898
- if (left <= 0) {
1899
- clearInterval(autoState.cdIv); autoState.cdIv = null; setCd(0)
1900
- try { localStorage.setItem('dsh-auto-memory.autoCont.last', String(Date.now())) } catch (eL) {}
1901
- oneClickContinue()
1902
- } else setCd(left)
1903
- }, 1000)
1904
- }
1905
2041
  async function oneClickContinue() {
2042
+ // 一键接续 = 共用执行器(刷新仪式 → 材料 → workspaceId 建会话 → 沿用模型 → 注入),与自动接续同一路径
1906
2043
  if (contBusy) return
1907
2044
  setContBusy(true)
1908
- setContMsg(locale === 'zh' ? '正在构造交接材料(含旧会话转写)…' : 'Preparing handoff material (incl. prev-session transcript)…')
1909
2045
  try {
1910
- var d = await apiPost(API.handoffContinue, {})
1911
- if (!d || !d.ok) throw new Error((d && d.error) || 'no handoff material')
1912
- var rf = remoteFace
1913
- if (!rf || !rf.session || typeof rf.session.create !== 'function') throw new Error(locale === 'zh' ? 'harness 未提供 remote.session(版本过旧?)' : 'remote.session unavailable (harness too old?)')
1914
- setContMsg(locale === 'zh' ? '正在创建新会话(沿用旧工作区与权限)…' : 'Creating session (same workspace & permissions)…')
1915
- // M-CM6-B v2:同 cwd 创建 → 工作区绑定/权限状态与旧会话一致;失败回退无参创建
1916
- var created
1917
- try { created = await rf.session.create(d.ws ? { cwd: d.ws } : {}) } catch (eCwd) { created = await rf.session.create({}) }
1918
- var newId = created && (created.sessionId || created.id)
1919
- if (!newId) throw new Error('create: no sessionId')
1920
- // 沿用旧会话模型(provider/model 由 host 从旧会话 request/context 折叠而来;不可用则静默回退路由默认)
1921
- if (d.provider && d.model && typeof rf.session.selectModel === 'function') {
1922
- setContMsg(locale === 'zh' ? '正在沿用旧模型 ' + d.model + '…' : 'Restoring model ' + d.model + '…')
1923
- try { await rf.session.selectModel({ sessionId: newId, provider: d.provider, model: d.model }) } catch (eMdl) {}
1924
- }
1925
- try { if (sessions && typeof sessions.open === 'function') sessions.open(newId) } catch (eOpen) {}
1926
- var reqId = (window.crypto && typeof crypto.randomUUID === 'function') ? crypto.randomUUID() : 'amc-' + Date.now() + '-' + Math.random().toString(36).slice(2)
1927
- await rf.session.prompt({ requestId: reqId, sessionId: newId, mode: 'queue', content: [{ type: 'text', text: d.carryText }] })
1928
- setContMsg(locale === 'zh' ? '✓ 已创建新会话(模型/工作区已沿用)并预载交接材料' : '✓ Session created (model/workspace restored) with ledger injected')
2046
+ var r = await runContinueFlow({ onProgress: setContMsg })
2047
+ setContMsg((locale === 'zh' ? '✓ 已创建新会话(工作区/模型已沿用)并预载交接材料' : '✓ Session created (workspace/model restored) with ledger injected') + (r && r.model ? ' · ' + r.model + (r.reasoningEffort ? '/' + r.reasoningEffort : '') : ''))
1929
2048
  } catch (e) {
1930
2049
  setContMsg('✗ ' + String(e && e.message ? e.message : e))
1931
2050
  } finally { setContBusy(false) }
@@ -1941,41 +2060,32 @@ window.__ModuleLoader__.load({
1941
2060
  apiGet(API.handoffState, { file: open }).then(function (d) { if (alive) setContent(d.text || '') }).catch(function () {})
1942
2061
  return function () { alive = false }
1943
2062
  }, [open])
1944
- useEffect(function () {
1945
- if (!autoCfg || !autoCfg.enabled) return
1946
- var alive = true
1947
- var tick = function () {
1948
- apiGet(API.handoffState).then(function (d) {
1949
- if (!alive || !d || !d.enabled) return
1950
- var wl = d.waterLevel || {}
1951
- if (!wl.window || !(wl.ratio >= (autoCfg.threshold || 0.7))) { autoState.lastTokens = null; return }
1952
- var idle = autoState.lastTokens !== null && wl.tokens === autoState.lastTokens
1953
- autoState.lastTokens = wl.tokens
1954
- if (!idle) return
1955
- var last = 0
1956
- try { last = Number(localStorage.getItem('dsh-auto-memory.autoCont.last') || 0) } catch (eL) {}
1957
- if (Date.now() - last < 30 * 60 * 1000) return
1958
- autoStartCountdown()
1959
- }).catch(function () {})
1960
- }
1961
- var iv = setInterval(tick, 20000)
1962
- tick()
1963
- return function () { alive = false; clearInterval(iv) }
1964
- }, [autoCfg && autoCfg.enabled, autoCfg && autoCfg.threshold])
2063
+ // 自动触发已移交全局 AutoContinueHost(①轮次边界 + ②确认卡);面板这里只保留手动一键接续与开关/阈值。
1965
2064
  useEffect(function () {
1966
2065
  return function () { if (autoState.cdIv) { clearInterval(autoState.cdIv); autoState.cdIv = null } }
1967
2066
  }, [])
2067
+ // 账本/计划版本按钮:窄面板下长文件名会被折成竖排,只显示短标签(时间),完整名放悬浮提示
2068
+ function shortLedgerName(name) {
2069
+ var m = /^(handoff|PLAN)-(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})\.md$/.exec(name || '')
2070
+ if (m) { var t = m[5] + ':' + m[6] + (m[1] === 'handoff' ? ':' + m[7] : ''); return (m[1] === 'PLAN' ? 'PLAN ' : '') + m[3] + '-' + m[4] + ' ' + t }
2071
+ var p = /^prev-session-.+-(\d{2})(\d{2})(\d{2})\.md$/.exec(name || '')
2072
+ if (p) return '↻ ' + p[1] + ':' + p[2] + ':' + p[3]
2073
+ return name
2074
+ }
1968
2075
  if (!data) return h(Loading)
1969
2076
  if (!data.enabled) return h(Card, { title: t('planTitle') }, h('div', { 'data-dam-hint': '' }, t('planDisabled')))
1970
2077
  var wl = data.waterLevel || {}
1971
2078
  var rows = [h(Card, { title: t('planTitle') }, h('div', { 'data-dam-content': '' }, data.plan || t('planEmpty')))]
1972
2079
  if (wl.window > 0) {
1973
- var pct = Math.min(Math.round((wl.ratio || 0) * 100), 150)
1974
- var barColor = pct >= 80 ? '#d4744f' : pct >= 50 ? '#d4a94f' : 'var(--dam-accent, #4f7cff)'
2080
+ // 2.2.4:百分比显示真实水位(旧版截断到 150% 会把「窗口算错」伪装成「刚好超一点」);进度条宽度仍按 100% 截断。
2081
+ var rawPct = Math.round((wl.ratio || 0) * 100)
2082
+ var pct = Math.min(rawPct, 999)
2083
+ var barColor = rawPct >= 80 ? '#d4744f' : rawPct >= 50 ? '#d4a94f' : 'var(--dam-accent, #4f7cff)'
2084
+ 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')))
1975
2085
  rows.unshift(h(Card, { title: t('waterCardTitle') }, h('div', { 'data-dam-content': '' }, [
1976
2086
  h('div', { style: { fontSize: 'calc(12px * var(--dam-scale))', marginBottom: '6px' } },
1977
2087
  (wl.tokens || 0).toLocaleString() + ' / ' + wl.window.toLocaleString() + ' token · ' + pct + '%',
1978
- ' ', h('span', { 'data-dam-hint': '' }, wl.source ? (wl.source.indexOf('auto') === 0 ? t('waterAutoSrc') + ': ' + wl.source.slice(5) : t('waterManualSrc')) : ''),
2088
+ ' ', h('span', { 'data-dam-hint': '' }, srcLabel),
1979
2089
  ' ', h('span', { 'data-dam-hint': '' }, wl.meter ? (wl.meter.indexOf('official') === 0 ? '· ' + t('waterMeterOfficial') + '(' + String(wl.meter).slice(9) + ')' : '· ' + t('waterMeterHeuristic')) : '')),
1980
2090
  h('div', { style: { height: '6px', borderRadius: '3px', background: 'color-mix(in srgb, currentColor 12%, transparent)', overflow: 'hidden' } },
1981
2091
  h('div', { style: { height: '100%', width: Math.min(pct, 100) + '%', background: barColor, borderRadius: '3px' } })),
@@ -1986,10 +2096,9 @@ window.__ModuleLoader__.load({
1986
2096
  rows.unshift(h(Card, { title: t('autoContTitle') }, h('div', { 'data-dam-content': '' }, [
1987
2097
  h('div', { 'data-dam-hint': '', style: { marginBottom: '6px' } }, t('autoContHint')),
1988
2098
  h('div', { 'data-dam-row': '' },
1989
- h('button', { 'data-dam-btn': '', onClick: function () { autoCancel(); autoSave({ autoContinueEnabled: !autoCfg.enabled }) } }, autoCfg.enabled ? t('autoContOn') : t('autoContOff')),
2099
+ h('button', { 'data-dam-btn': '', onClick: function () { autoSave({ autoContinueEnabled: !autoCfg.enabled }) } }, autoCfg.enabled ? t('autoContOn') : t('autoContOff')),
1990
2100
  h('label', { 'data-dam-hint': '', style: { marginLeft: '8px' } }, t('autoContThreshold') + ' ',
1991
2101
  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 }) } }))),
1992
- 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,
1993
2102
  ])))
1994
2103
  }
1995
2104
  if (open) {
@@ -1998,11 +2107,11 @@ window.__ModuleLoader__.load({
1998
2107
  } else {
1999
2108
  if (data.planVersions && data.planVersions.length) {
2000
2109
  rows.push(h(Card, { title: t('planVersions') }, h('div', { 'data-dam-row': '' }, data.planVersions.map(function (v) {
2001
- return h('button', { 'data-dam-btn': '', key: v.name, onClick: function () { setOpen(v.name) } }, v.name)
2110
+ return h('button', { 'data-dam-btn': '', key: v.name, title: v.name, onClick: function () { setOpen(v.name) } }, shortLedgerName(v.name))
2002
2111
  }))))
2003
2112
  }
2004
2113
  rows.push(h(Card, { title: t('planLedgers') }, h('div', { 'data-dam-row': '' }, (data.ledgers || []).length ? data.ledgers.map(function (v) {
2005
- return h('button', { 'data-dam-btn': '', key: v.name, onClick: function () { setOpen(v.name) } }, v.name)
2114
+ return h('button', { 'data-dam-btn': '', key: v.name, title: v.name, onClick: function () { setOpen(v.name) } }, shortLedgerName(v.name))
2006
2115
  }) : h('div', { 'data-dam-muted': '' }, t('empty')))))
2007
2116
  // M-CM6-B 一键接续:写账本→建新会话→预载交接材料→自动切换
2008
2117
  rows.push(h(Card, { title: t('continueCardTitle') }, h('div', { 'data-dam-content': '' }, [
@@ -3281,9 +3390,10 @@ window.__ModuleLoader__.load({
3281
3390
  // ───────────────────────── 设置页 ─────────────────────────
3282
3391
  var STYLE_IDS = ['auto', 'life', 'professional']
3283
3392
  var LOCALE_IDS_LIST = ['system', 'zh', 'en']
3284
- // M-CM6-C v2·常驻自动接续 watcher(2026-09-08):全局生效,与面板/页签是否打开无关。
3285
- // 条件:autoContinueEnabled !== false(缺省=开)&& 水位≥阈值(缺省0.8)&& 连续两轮 tokens 不增长(会话空闲)
3286
- // && 距上次触发>30min → 30s 可取消倒计时 → 自动执行接续(写账本→建会话→切换→注入材料)。
3393
+ // M-CM6-C v3·常驻自动接续 watcher(2026-09-08 2.2.4):全局生效,与面板/页签是否打开无关。
3394
+ // ①触发 = autoContinueEnabled !== false && 水位≥阈值 && harness 权威 running==false(轮次边界,取代旧"两轮水位持平")
3395
+ // + 3s 短确认(防刚结束又起新一轮)→ ②确认卡(同意 / 拒绝 / 超时 N 秒自动接续;拒绝记录在案,同一边界不再提示)
3396
+ // → ③刷新仪式(旧 Agent 刷 PLAN+账本)→ 一键接续(workspaceId 建会话 → 沿用模型 → 注入材料)。
3287
3397
  function AutoContinueHost() {
3288
3398
  var cfgP = useState(null)
3289
3399
  var acCfg = cfgP[0]
@@ -3297,6 +3407,9 @@ window.__ModuleLoader__.load({
3297
3407
  var bsP = useState(false)
3298
3408
  var acBusy = bsP[0]
3299
3409
  var setAcBusy = bsP[1]
3410
+ var cfP = useState(null)
3411
+ var acConfirm = cfP[0]
3412
+ var setAcConfirm = cfP[1]
3300
3413
  useEffect(function () {
3301
3414
  var alive = true
3302
3415
  var load = function () {
@@ -3310,73 +3423,115 @@ window.__ModuleLoader__.load({
3310
3423
  var on = !(acCfg && acCfg.autoContinueEnabled === false)
3311
3424
  if (!on) return
3312
3425
  var thr = (acCfg && Number(acCfg.autoContinueThreshold)) || 0.8
3426
+ var confirmSec = Number(acCfg && acCfg.autoContinueConfirmSeconds)
3427
+ if (!(confirmSec >= 10 && confirmSec <= 120)) confirmSec = 35
3428
+ var cooldownMin = Number(acCfg && acCfg.autoContinueCooldownMinutes)
3429
+ if (!(cooldownMin > 0)) cooldownMin = 30
3313
3430
  var alive = true
3314
- var tick = function () {
3431
+ var maybePrompt = function (edgeAt) {
3432
+ if (!alive || acBusy || autoState.cdIv || autoState.promptVisible) return
3433
+ if (!edgeAt || Date.now() - edgeAt > 10 * 60 * 1000) return // 边界太旧不再提示
3434
+ if (Date.now() < autoState.suppressUntil) return
3435
+ if (autoState.rejectedEdgeAt === edgeAt) return // ②拒绝过同一边界
3315
3436
  apiGet(API.handoffState).then(function (d) {
3316
3437
  if (!alive || !d || !d.enabled) return
3317
3438
  var wl = d.waterLevel || {}
3318
- if (!wl.window || !(wl.ratio >= thr)) { autoState.lastTokens = null; return }
3319
- var idle = autoState.lastTokens !== null && wl.tokens === autoState.lastTokens
3320
- autoState.lastTokens = wl.tokens
3321
- if (!idle || autoState.cdIv || acBusy) return
3439
+ if (!wl.window || !(wl.ratio >= thr)) return
3322
3440
  var last = 0
3323
3441
  try { last = Number(localStorage.getItem('dsh-auto-memory.autoCont.last') || 0) } catch (eL) {}
3324
- if (Date.now() - last < 30 * 60 * 1000) return
3325
- setAcSt(null)
3326
- var left = 30
3442
+ if (Date.now() - last < cooldownMin * 60 * 1000) return
3443
+ if (autoState.promptVisible || autoState.cdIv) return
3444
+ setAcConfirm({ ratio: Number(wl.ratio) || 0, tokens: Number(wl.tokens) || 0, window: Number(wl.window) || 0, edgeAt: edgeAt })
3445
+ setAcSt('')
3446
+ autoState.promptVisible = true
3447
+ var left = confirmSec
3327
3448
  setAcCd(left)
3328
3449
  autoState.cdIv = setInterval(function () {
3329
3450
  left -= 1
3330
3451
  if (left <= 0) {
3331
3452
  clearInterval(autoState.cdIv); autoState.cdIv = null; setAcCd(0)
3332
- runAutoContinue()
3453
+ autoState.promptVisible = false; setAcConfirm(null)
3454
+ runAuto() // ③超时 = 挂机 → 自动接续兜底
3333
3455
  } else setAcCd(left)
3334
3456
  }, 1000)
3335
3457
  }).catch(function () {})
3336
3458
  }
3337
- var iv = setInterval(tick, 20000)
3338
- tick()
3339
- return function () { alive = false; clearInterval(iv) }
3340
- }, [acCfg && acCfg.autoContinueEnabled === false, acCfg && (Number(acCfg.autoContinueThreshold) || 0.8), acBusy])
3459
+ // 轮次边界:running true→false 边沿 + 3s 短确认
3460
+ var verifyEdge = function (edgeAt) {
3461
+ if (!alive || autoState.edgeAt !== edgeAt) return
3462
+ var info = currentRunningInfo()
3463
+ if (info && info.running) return // 短确认失败:新一轮已开始,不算边界
3464
+ maybePrompt(edgeAt)
3465
+ }
3466
+ var armEdge = function () {
3467
+ autoState.edgeAt = Date.now()
3468
+ var info = currentRunningInfo()
3469
+ autoState.edgeSession = info ? info.sessionId : ''
3470
+ var edgeAt = autoState.edgeAt
3471
+ setTimeout(function () { verifyEdge(edgeAt) }, 3000)
3472
+ }
3473
+ var observe = function () {
3474
+ if (!alive) return
3475
+ var info = currentRunningInfo()
3476
+ if (!info) return
3477
+ if (autoState.lastRunning === null) {
3478
+ autoState.lastRunning = info.running
3479
+ if (!info.running) armEdge() // 首次观察即空闲(挂机/刚进会话):按边界处理
3480
+ return
3481
+ }
3482
+ if (autoState.lastRunning === true && info.running === false) armEdge()
3483
+ autoState.lastRunning = info.running
3484
+ }
3485
+ var unsub = null
3486
+ try { if (sessions && sessions.list && typeof sessions.list.subscribe === 'function') unsub = sessions.list.subscribe(observe) } catch (eSub) {}
3487
+ observe()
3488
+ var iv = setInterval(function () { observe(); maybePrompt(autoState.edgeAt) }, 20000) // 兜底:水位后到 / 订阅不可用
3489
+ return function () {
3490
+ alive = false
3491
+ clearInterval(iv)
3492
+ if (unsub) { try { unsub() } catch (eU) {} }
3493
+ }
3494
+ }, [acCfg && acCfg.autoContinueEnabled === false, acCfg && (Number(acCfg.autoContinueThreshold) || 0.8), acCfg && acCfg.autoContinueConfirmSeconds, acCfg && acCfg.autoContinueCooldownMinutes, acBusy])
3341
3495
  var cancelAc = function () {
3342
3496
  if (autoState.cdIv) { clearInterval(autoState.cdIv); autoState.cdIv = null }
3497
+ autoState.promptVisible = false
3343
3498
  setAcCd(0)
3344
3499
  }
3345
- var runAutoContinue = function () {
3500
+ var runAuto = function () {
3346
3501
  if (acBusy) return
3502
+ cancelAc()
3503
+ setAcConfirm(null)
3347
3504
  if (!remoteFace || !remoteFace.session || typeof remoteFace.session.create !== 'function') {
3348
3505
  setAcSt(locale === 'zh' ? '✗ remote.session 不可用——请重启 dsh web 后重试' : '✗ remote.session unavailable — restart dsh web')
3349
3506
  return
3350
3507
  }
3351
3508
  setAcBusy(true)
3352
- setAcSt(locale === 'zh' ? '正在自动接续:写账本→建新会话→注入材料…' : 'Auto-continuing: ledger → new session → inject…')
3353
- apiPost(API.handoffContinue, {}).then(function (d) {
3354
- if (!d || !d.ok) throw new Error((d && d.error) || 'no handoff material')
3355
- var p
3356
- try { p = remoteFace.session.create(d.ws ? { cwd: d.ws } : {}) } catch (eC) { p = remoteFace.session.create({}) }
3357
- return p.then(function (created) {
3358
- var newId = created && (created.sessionId || created.id)
3359
- if (!newId) throw new Error('create: no sessionId')
3360
- var chain = Promise.resolve()
3361
- if (d.provider && d.model && typeof remoteFace.session.selectModel === 'function') {
3362
- chain = chain.then(function () { try { return remoteFace.session.selectModel({ sessionId: newId, provider: d.provider, model: d.model }) } catch (eM) { return null } })
3363
- }
3364
- return chain.then(function () {
3365
- try { if (sessions && typeof sessions.open === 'function') sessions.open(newId) } catch (eO) {}
3366
- var reqId = (window.crypto && typeof crypto.randomUUID === 'function') ? crypto.randomUUID() : 'ac-' + Date.now() + '-' + Math.random().toString(36).slice(2)
3367
- return remoteFace.session.prompt({ requestId: reqId, sessionId: newId, mode: 'queue', content: [{ type: 'text', text: d.carryText }] })
3368
- })
3369
- })
3370
- }).then(function () {
3371
- try { localStorage.setItem('dsh-auto-memory.autoCont.last', String(Date.now())) } catch (eL) {}
3372
- setAcSt(locale === 'zh' ? '✓ 已自动接续到新会话,可直接上手' : '✓ Auto-continued into a new session')
3509
+ setAcSt(locale === 'zh' ? '正在自动接续:刷新白板/账本 → 建新会话 → 注入材料…' : 'Auto-continuing: refresh PLAN/ledger → new session → inject…')
3510
+ runContinueFlow({ onProgress: function (m) { setAcSt(m) } }).then(function (r) {
3511
+ setAcSt((locale === 'zh' ? '✓ 已自动接续到新会话,可直接上手' : '✓ Auto-continued into a new session') + (r && r.model ? ' · ' + r.model + (r.reasoningEffort ? '/' + r.reasoningEffort : '') : ''))
3373
3512
  setAcBusy(false)
3374
3513
  }).catch(function (e) { setAcSt('✗ ' + String(e && e.message ? e.message : e)); setAcBusy(false) })
3375
3514
  }
3376
- if (acCd > 0 || acSt) {
3515
+ var onAgree = function () { runAuto() }
3516
+ var onReject = function () {
3517
+ var edgeAt = (acConfirm && acConfirm.edgeAt) || autoState.edgeAt || 0
3518
+ cancelAc()
3519
+ setAcConfirm(null)
3520
+ autoState.rejectedEdgeAt = edgeAt
3521
+ setAcSt(t('autoContRejected'))
3522
+ }
3523
+ if (acConfirm || acCd > 0 || acSt) {
3377
3524
  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)' } },
3378
- 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,
3379
- acSt ? h('div', { style: acCd > 0 ? { marginTop: '4px' } : {} }, acSt) : null)
3525
+ acConfirm ? h('div', { 'data-dam-autocont-confirm': '' }, [
3526
+ h('div', { style: { marginBottom: '4px' } }, t('autoContConfirm').replace('{p}', String(Math.round((acConfirm.ratio || 0) * 100)))),
3527
+ h('div', { 'data-dam-hint': '' }, (acConfirm.tokens || 0).toLocaleString() + ' / ' + (acConfirm.window || 0).toLocaleString() + ' token'),
3528
+ h('div', { style: { marginTop: '8px' } },
3529
+ h('button', { 'data-dam-btn': '', onClick: onAgree }, t('autoContAgree')),
3530
+ h('button', { 'data-dam-btn': '', style: { marginLeft: '6px' }, onClick: onReject }, t('autoContReject')),
3531
+ acCd > 0 ? h('span', { 'data-dam-hint': '', style: { marginLeft: '8px' } }, t('autoContTimeout').replace('{s}', String(acCd))) : null),
3532
+ ]) : null,
3533
+ !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,
3534
+ acSt ? h('div', { style: (acConfirm || acCd > 0) ? { marginTop: '4px' } : {} }, acSt) : null)
3380
3535
  }
3381
3536
  return null
3382
3537
  }
@@ -3907,7 +4062,11 @@ window.__ModuleLoader__.load({
3907
4062
  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')),
3908
4063
  field(t('fWaterThreshold'), h('input', { 'data-dam-input': '', type: 'number', min: 0.1, max: 1.5, step: 0.05, value: cfg.waterLevelThreshold === undefined ? 0.8 : cfg.waterLevelThreshold, onChange: function (e) { set('waterLevelThreshold', Number(e.target.value) || 0.8) } }), t('fWaterThresholdHint')),
3909
4064
  field(t('fWaterAdvisory'), h('input', { type: 'checkbox', checked: cfg.waterLevelAdvisory !== false, onChange: function (e) { set('waterLevelAdvisory', e.target.checked) } }), t('fWaterAdvisoryHint')),
3910
- field(t('fWaterAuto'), h('input', { type: 'checkbox', checked: cfg.waterLevelAutoHandoff !== false, onChange: function (e) { set('waterLevelAutoHandoff', e.target.checked) } }), t('fWaterAutoHint'))]),
4065
+ field(t('fWaterAuto'), h('input', { type: 'checkbox', checked: cfg.waterLevelAutoHandoff !== false, onChange: function (e) { set('waterLevelAutoHandoff', e.target.checked) } }), t('fWaterAutoHint')),
4066
+ field(locale === 'zh' ? '子代理痕迹回收' : 'Subagent trace recycle', h('input', { type: 'checkbox', checked: cfg.subagentGcEnabled !== false, onChange: function (e) { set('subagentGcEnabled', e.target.checked) } }),
4067
+ 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.'),
4068
+ 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) } }),
4069
+ 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.')]),
3911
4070
  section('maintenance', sectionLabels.maintenance, [field(t('fVersion'), h('div', { 'data-dam-row': '' },
3912
4071
  h('span', { style: { flex: 1 } }, verInfo ? (verInfo.current || '?') + (verInfo.latest ? ' → ' + verInfo.latest + (verInfo.upToDate ? ' ' + t('upToDate') : ' ' + t('hasUpdate')) : '') : (checkingUpdate ? t('checking') : '—')),
3913
4072
  h('button', { 'data-dam-btn': '', onClick: checkUpdate, disabled: checkingUpdate }, checkingUpdate ? t('checking') : t('checkUpdate')),
@@ -4167,7 +4326,7 @@ window.__ModuleLoader__.load({
4167
4326
  console.log('[dsh-auto-memory] client ready: sidebar entry + panel + settings page')
4168
4327
  }
4169
4328
 
4170
- exports.inject = ['slots', 'sessions']
4329
+ exports.inject = ['slots', 'sessions', 'remote', 'remote.session']
4171
4330
  exports.apply = apply
4172
4331
  return module.exports
4173
4332
  },