@a9i5k4/dsh-auto-memory 2.3.1 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * 3. settings.section — 自动记忆设置页(存储位置、注入、反思风格)
8
8
  * Data flows over /api/dsh-auto-memory/* (loopback-only host routes).
9
9
  */
10
- console.log('[dsh-auto-memory] client v0.1.13 fingerprint: drawer-summary-ok')
10
+ console.log('[dsh-auto-memory] client v2.4.1 fingerprint: drawer-summary-ok')
11
11
  window.__ModuleLoader__.load({
12
12
  id: '@a9i5k4/dsh-auto-memory',
13
13
  factory: (require) => {
@@ -252,6 +252,8 @@ window.__ModuleLoader__.load({
252
252
  fProjectDir: '项目记忆目录', fProjectDirHint: '相对各工作区的目录名(默认 .dsh-memory)。',
253
253
  fInject: '注入记忆上下文', fInjectHint: '每次组装提示词时自动注入 <memory_system> 块。',
254
254
  fBudget: '注入预算(字符)', fBudgetHint: '记忆块总预算,超出部分截断。默认 1600(≈400-600 token/轮);活跃会话每轮都注入这段背景,调低更省 token,调高保留更多记忆。',
255
+ fNoteCap: '项目笔记容量上限(字符)', fNoteCapHint: '项目笔记 MEMORY.md 的容量上限,默认 12000。超出时自动整理:先把较早内容交给模型折叠成要点,失败则退回整条归档(原文进 archive,不丢)——新记忆不会被堵在外面。注意与上面的「注入预算」不是一回事:这个管文件本体大小,注入预算管每轮往上下文塞多少摘要。',
256
+ fUserCap: '用户级记忆容量上限(字符)', fUserCapHint: '用户级 MEMORY.md(跨项目规则)的容量上限,默认 12000。整理方式与项目笔记相同(先折叠成要点,必要时整条归档)。',
255
257
  fSnapGap: '快照最小间隔(轮)', fSnapGapHint: '动态记忆快照内容变化后,至少隔 N 轮才重新注入,避免每轮日志微变都追加快照导致历史膨胀。默认 5;0=每轮都尝试(仍受内容变化约束)。',
256
258
  fReinjectOnCompact: '压缩后立即重注入快照', fReinjectOnCompactHint: '上下文被压缩/截断(通常伴随 contextVersion 重置)后,快照会被清掉;开启后强制立即重注入一次,确保记忆背景重建。默认开。',
257
259
  fPromptCustom: '自定义记忆注入 prompt', fPromptCustomHint: '可覆盖各层提示文案(小众功能)。支持占位符 {date} {ws} {budget} {n}。改坏了可一键恢复默认。',
@@ -282,7 +284,7 @@ window.__ModuleLoader__.load({
282
284
  fMaintSchedule: '定时 30 天蒸馏', fMaintScheduleHint: '每天到点自动把超过 30 天的旧日志蒸馏归档(等价 memory_maintain);无旧日志时零成本跳过。',
283
285
  fMaintScheduleTime: '蒸馏触发时间', fMaintScheduleTimeHint: 'HH:MM,默认 10:00(与固化时间错开)。',
284
286
  waterCardTitle: '上下文水位', waterAutoSrc: '自动检测', waterManualSrc: '手动设定', waterOfficialSrc: '官方路由容量', waterFallbackSrc: '回退默认值', waterNotMeasured: '本会话尚未测量(发送一轮后自动更新)', waterMeterOfficial: '官方计量', waterMeterHeuristic: '启发式降级', waterThresholdHint: '水位达到 {t} 时注入交接建议并自动补写账本;计量优先用官方 token-meter(与聊天框 context ring 同源),不可用时降级启发式估算;窗口优先取官方路由容量(request/context 的 contextWindow),其次按当前模型查 settings.yaml contextWindow,也可在下方手动覆盖。',
285
- autoContTitle: '自动接续', autoContHint: '开启后:水位达到阈值、且旧会话已空闲(harness 权威 running 位在轮次边界转为 false,取代旧「两轮水位持平」)时,弹出确认卡——同意=立即接续,拒绝=本轮跳过(同一边界不再提示),30-40 秒无操作视为挂机自动接续;接续前先让旧 Agent 刷新白板 PLAN 与交接账本,再建新会话(沿用工作区/模型/思考档位)并注入分层交接材料;触发后默认 30 分钟内不重复。', autoContOn: '自动接续:开', autoContOff: '自动接续:关', autoContThreshold: '阈值', autoContCountdown: '⏳ {s} 秒后自动接续…', autoContCancel: '取消', autoContConfirm: '本会话水位已达 {p}%,旧会话已空闲(轮次边界)——现在接续到新会话?', autoContAgree: '同意接续', autoContReject: '拒绝', autoContTimeout: '{s} 秒后自动接续(视为挂机)', autoContRejected: '已跳过本次接续(下一个轮次边界再提醒)',
287
+ autoContTitle: '自动接续', autoContHint: '开启后:水位达到阈值、且旧会话已空闲(harness 权威 running 位在轮次边界转为 false,取代旧「两轮水位持平」)时,弹出确认卡——同意=立即接续,拒绝=本轮跳过(同一边界不再提示),30-40 秒无操作视为挂机自动接续;接续前先让旧 Agent 刷新白板 PLAN 与交接账本,再建新会话(沿用工作区/模型/思考档位)并注入分层交接材料;触发后默认 30 分钟内不重复。', autoContOn: '自动接续:开', autoContOff: '自动接续:关', autoContThreshold: '阈值', autoContCountdown: '⏳ {s} 秒后自动接续…', autoContCancel: '取消', autoContConfirm: '本会话「可用额度」已用 {p}%(小圈读数见下行),旧会话已空闲(轮次边界)——现在接续到新会话?', autoContAgree: '同意接续', autoContReject: '拒绝', autoContTimeout: '{s} 秒后自动接续(视为挂机)', autoContRejected: '已跳过本次接续(下一个轮次边界再提醒)',
286
288
  continueCardTitle: '一键接续', continueCardHint: '写好交接账本→创建新会话→把交接材料作为首条消息预载,并自动切换到新会话继续任务(接续材料含白板节选+最新账本;当前工作区无账本时自动取全局最近一篇)。', continueBtn: '一键接续到新会话', continueBusy: '处理中…',
287
289
  fStyle: '反思风格', fStyleHint: '生活化 / 专业性 / 由内容决定。',
288
290
  fLocale: '界面语言', fLocaleHint: '默认跟随 DSH 系统语言;也可手动指定中文 / English。', followSystem: '跟随系统语言',
@@ -401,6 +403,8 @@ window.__ModuleLoader__.load({
401
403
  fProjectDir: 'Project memory dir', fProjectDirHint: 'Directory name relative to each workspace (default .dsh-memory).',
402
404
  fInject: 'Inject memory context', fInjectHint: 'Auto-inject <memory_system> block into every prompt.',
403
405
  fBudget: 'Injection budget (chars)', fBudgetHint: 'Total budget for the memory block; excess is truncated. Default 1600 (~400-600 tokens/turn); this background is injected every turn, so lower = fewer tokens, higher = more memory retained.',
406
+ fNoteCap: 'Project note capacity (chars)', fNoteCapHint: 'Capacity cap for the project MEMORY.md, default 12000. When exceeded the framework auto-consolidates: older content is first folded into bullet points by the model, falling back to whole-record archiving (originals go to archive, never lost) — new memories are never blocked. This is NOT the injection budget above: this caps the file itself, the injection budget caps the digest added to each turn.',
407
+ fUserCap: 'User memory capacity (chars)', fUserCapHint: 'Capacity cap for the user-level MEMORY.md (cross-project rules), default 12000. Same consolidation behaviour (fold into points first, whole-record archive as fallback).',
404
408
  fSnapGap: 'Snapshot min gap (turns)', fSnapGapHint: 'After the dynamic memory snapshot changes, re-inject at least N turns later, so small per-turn log changes do not append a new snapshot every turn (history bloat). Default 5; 0 = try every turn (still change-gated).',
405
409
  fReinjectOnCompact: 'Re-inject snapshot immediately after compaction', fReinjectOnCompactHint: 'When the context is compacted/truncated (usually a contextVersion reset), the snapshot is cleared; on = force re-inject once so the memory background rebuilds. Default on.',
406
410
  fPromptCustom: 'Customize memory-injection prompt', fPromptCustomHint: 'Override any prompt layer (power feature). Placeholders: {date} {ws} {budget} {n}. One-click reset restores defaults.',
@@ -563,6 +567,30 @@ window.__ModuleLoader__.load({
563
567
 
564
568
  // ───────────────────────── 更新弹窗 / 首次指导 ─────────────────────────
565
569
  var CHANGELOG = {
570
+ '2.4.1': { zh: [
571
+ '修复:已接续过的会话会被再次触发接续(重复建窗口)。新增「已接续闩锁」并落盘(session id 为键、重启有效):宿主路径在交接材料投料成功后落闩,浏览器路径在新会话真建成后落闩,失败不落闩以便重试。',
572
+ '修复:宿主已完成接续后,确认卡仍停在「同意接续/拒绝」并与「已自动接续到新会话」并存;arm 状态改为按会话过滤,不再在其他窗口弹「本会话水位已达」。',
573
+ '修复:水位双口径那一行(本会话水位 / 官方小圈读数 / 距硬墙余量)此前因漏拷贝 ring/wall 永不渲染。',
574
+ '内部:界面路径表引入唯一前缀常量与一致性锁(客户端路径必须都在宿主表内、表外零裸字面量);写配置 4 条路径收敛为一个出口;10 处重复的语义状态刷新收敛为一处。',
575
+ '流程:自本版起,发版必须同步 CHANGELOG、应用内更新说明与版本标识(release.mjs 已加校验,缺一即拒绝构建)。',
576
+ ], en: [
577
+ 'Fix: a session that had already been continued could be armed again, creating duplicate windows. A persisted continuation latch (keyed by session id, survives restarts) is now written only after the handoff material is actually delivered (host path) or the new session really exists (browser path); failures do not latch, so retries still work.',
578
+ 'Fix: after the host finished continuing, the confirm card still sat on Agree/Decline next to "continued into a new session"; arming is now filtered per session, so other windows no longer show "this session reached the water level".',
579
+ 'Fix: the dual-scale water line (session level / official ring reading / distance to the hard wall) never rendered because ring/wall were not copied through.',
580
+ 'Internal: one prefix constant plus a consistency lock for the panel route table, a single config-write exit, and one shared semantic-status refresh replacing ten copies.',
581
+ 'Process: from this version on, releases must update CHANGELOG, in-app release notes and version identifiers together (release.mjs now refuses to build otherwise).',
582
+ ] },
583
+ '2.4.0': { zh: [
584
+ '★ 水位判据按「该会话当前模型」的预留额度自适应:消息可用上限 = 窗口 − 该路由的 maxTokens(实测 384,000 预留 → 可用 616,000),阈值随之落在健康区间;压缩与 CONTEXT_WINDOW_EXCEEDED 升为硬触发。',
585
+ '★ 修复自动接续静默失效:DSH 0.1.5 起 requestId 必填,宿主兜底接续漏传该字段时官方抛错被包装成误导性的 "prompt rejected" —— 现象是新会话建出来了、交接材料却从未送达。',
586
+ '★ 记忆容量口径重做:改为「文件容量上限」(noteCapacityChars / userCapacityChars,默认各 12000 字符),先 AI 折叠成要点、失败退回整条归档,写入不再被堵死。',
587
+ '★ Python 语义引擎贯通:安装向导写入正确的工作目录、模型落位与 onnxFile 契约对齐、向导常驻设置页,装了就能用。',
588
+ ], en: [
589
+ '★ Water-level judgement now adapts to the reserve of the session\'s current route: usable budget = window − that route\'s maxTokens (measured 384,000 reserve → 616,000 usable), so thresholds land in a healthy range; compaction and CONTEXT_WINDOW_EXCEEDED became hard triggers.',
590
+ '★ Fixed silent auto-continue failure: since DSH 0.1.5 requestId is required, and the host fallback omitted it, so the official error surfaced as a misleading "prompt rejected" — the new session existed but the handoff material never arrived.',
591
+ '★ Memory capacity reworked around per-file character caps (noteCapacityChars / userCapacityChars, 12000 each): fold into key points first, fall back to whole-entry archiving, so writes are never blocked.',
592
+ '★ Python semantic engine works end to end: the wizard writes the right working directory, model placement and onnxFile contracts line up, and the wizard is always reachable from Settings.',
593
+ ] },
566
594
  '2.3.0': { zh: [
567
595
  '★ 检索改为分层语义唤回:memory_recall 先返回 L0 摘要列表(每条记忆约 90 字,含 id/得分/匹配原因),需要原文再按 id 展开——此前全文直接进 embedding,超过模型 512 token 上限的条目尾部全部丢失(实测最长一条 9,822 字符);现在摘要永远在限制内,长记忆不再有损。',
568
596
  '★ 检索融合改为三臂 rank-space:词法 + 语义 + 时间 三路各自排名后按 RRF(k=60)融合,取代旧的 minmax 归一化加权——旧方案分数只反映「这一批里排第几」,候选集一换分数就漂移、候选只剩 1 条时退化为常数,而「要不要注入」的决策恰恰依赖分数与阈值比较。现在排序与决策解耦,分数不再说谎。',
@@ -851,44 +879,56 @@ window.__ModuleLoader__.load({
851
879
  }
852
880
 
853
881
  // ───────────────────────── API ─────────────────────────
882
+ // 路由前缀唯一常量(2026-09-10 第 0 步止血):表中每条路径由它拼出,不再逐个重复全串。
883
+ var ROUTE_PREFIX = '/api/dsh-auto-memory'
884
+ // 宿主半边(lib/index.js 的 API)声明的是同一批路径 —— 两个半边是独立 bundle(本文件经 __ModuleLoader__ 手写加载,
885
+ // 无法 import 宿主模块),所以"只有一份"这件事由 tests/smoke/ 下的路径表一致性锁保证:
886
+ // 客户端路径 ⊆ 宿主路径、宿主独有路径只允许显式白名单、本文件表外不得再出现裸路径字面量。
854
887
  var API = {
855
- state: '/api/dsh-auto-memory/state',
856
- list: '/api/dsh-auto-memory/list',
857
- file: '/api/dsh-auto-memory/file',
858
- recall: '/api/dsh-auto-memory/recall',
859
- smartRecall: '/api/dsh-auto-memory/smart-recall',
860
- workspaces: '/api/dsh-auto-memory/workspaces',
861
- debug: '/api/dsh-auto-memory/debug',
862
- scanDirty: '/api/dsh-auto-memory/scan-dirty',
863
- browseDir: '/api/dsh-auto-memory/browse-dir',
864
- pickDir: '/api/dsh-auto-memory/pick-dir',
865
- updateCheck: '/api/dsh-auto-memory/update-check',
866
- update: '/api/dsh-auto-memory/update',
867
- config: '/api/dsh-auto-memory/config',
868
- reflect: '/api/dsh-auto-memory/reflect',
869
- reflectAuto: '/api/dsh-auto-memory/reflect-auto',
870
- note: '/api/dsh-auto-memory/note',
871
- external: '/api/dsh-auto-memory/external',
872
- externalView: '/api/dsh-auto-memory/external-view',
873
- externalRemove: '/api/dsh-auto-memory/external-remove',
874
- externalImport: '/api/dsh-auto-memory/external-import',
875
- calendar: '/api/dsh-auto-memory/calendar',
876
- summarize: '/api/dsh-auto-memory/summarize',
877
- greet: '/api/dsh-auto-memory/greet',
878
- notices: '/api/dsh-auto-memory/notices',
879
- models: '/api/dsh-auto-memory/models',
880
- semanticDeepDetect: '/api/dsh-auto-memory/semantic-deep-detect',
881
- pyDetect: '/api/dsh-auto-memory/python-setup/detect',
882
- pyVenv: '/api/dsh-auto-memory/python-setup/venv',
883
- pyDeps: '/api/dsh-auto-memory/python-setup/deps',
884
- pyModel: '/api/dsh-auto-memory/python-setup/model',
885
- pyCancel: '/api/dsh-auto-memory/python-setup/cancel',
886
- pyStatus: '/api/dsh-auto-memory/python-setup/status',
887
- handoffState: '/api/dsh-auto-memory/handoff-state',
888
- handoffContinue: '/api/dsh-auto-memory/handoff-continue',
889
- autoContState: '/api/dsh-auto-memory/auto-continue-state',
890
- autoContDecide: '/api/dsh-auto-memory/auto-continue-decide',
891
- semanticDownload: '/api/dsh-auto-memory/semantic-download',
888
+ state: ROUTE_PREFIX + '/state',
889
+ list: ROUTE_PREFIX + '/list',
890
+ file: ROUTE_PREFIX + '/file',
891
+ recall: ROUTE_PREFIX + '/recall',
892
+ smartRecall: ROUTE_PREFIX + '/smart-recall',
893
+ workspaces: ROUTE_PREFIX + '/workspaces',
894
+ debug: ROUTE_PREFIX + '/debug',
895
+ scanDirty: ROUTE_PREFIX + '/scan-dirty',
896
+ browseDir: ROUTE_PREFIX + '/browse-dir',
897
+ pickDir: ROUTE_PREFIX + '/pick-dir',
898
+ updateCheck: ROUTE_PREFIX + '/update-check',
899
+ update: ROUTE_PREFIX + '/update',
900
+ config: ROUTE_PREFIX + '/config',
901
+ reflect: ROUTE_PREFIX + '/reflect',
902
+ reflectAuto: ROUTE_PREFIX + '/reflect-auto',
903
+ note: ROUTE_PREFIX + '/note',
904
+ external: ROUTE_PREFIX + '/external',
905
+ externalView: ROUTE_PREFIX + '/external-view',
906
+ externalRemove: ROUTE_PREFIX + '/external-remove',
907
+ externalImport: ROUTE_PREFIX + '/external-import',
908
+ calendar: ROUTE_PREFIX + '/calendar',
909
+ summarize: ROUTE_PREFIX + '/summarize',
910
+ greet: ROUTE_PREFIX + '/greet',
911
+ notices: ROUTE_PREFIX + '/notices',
912
+ models: ROUTE_PREFIX + '/models',
913
+ semanticDeepDetect: ROUTE_PREFIX + '/semantic-deep-detect',
914
+ semanticStatus: ROUTE_PREFIX + '/semantic-status',
915
+ semanticEmit: ROUTE_PREFIX + '/semantic-emit',
916
+ pyDetect: ROUTE_PREFIX + '/python-setup/detect',
917
+ pyVenv: ROUTE_PREFIX + '/python-setup/venv',
918
+ pyDeps: ROUTE_PREFIX + '/python-setup/deps',
919
+ pyModel: ROUTE_PREFIX + '/python-setup/model',
920
+ pyCancel: ROUTE_PREFIX + '/python-setup/cancel',
921
+ pyStatus: ROUTE_PREFIX + '/python-setup/status',
922
+ handoffState: ROUTE_PREFIX + '/handoff-state',
923
+ handoffContinue: ROUTE_PREFIX + '/handoff-continue',
924
+ handoffPermission: ROUTE_PREFIX + '/handoff-permission',
925
+ autoContState: ROUTE_PREFIX + '/auto-continue-state',
926
+ autoContDecide: ROUTE_PREFIX + '/auto-continue-decide',
927
+ semanticDownload: ROUTE_PREFIX + '/semantic-download',
928
+ shadowRecent: ROUTE_PREFIX + '/shadow-recent',
929
+ reviewFeedback: ROUTE_PREFIX + '/review-feedback',
930
+ memoryHub: ROUTE_PREFIX + '/memory-hub',
931
+ storageManage: ROUTE_PREFIX + '/storage-manage',
892
932
  }
893
933
  function query(params) {
894
934
  var search = new URLSearchParams()
@@ -908,6 +948,28 @@ window.__ModuleLoader__.load({
908
948
  if (!res.ok) throw new Error(body.error || ('POST ' + path + ' → HTTP ' + res.status))
909
949
  return body
910
950
  }
951
+ /**
952
+ * 写配置的唯一出口(2026-09-10 第 0 步止血):此前有 4 条互不相同的写配置路径
953
+ * (设置页整对象 POST / 接续页 patch / 向导两处 patch),端点、后置刷新、错误处理各写一遍。
954
+ * 现在全部经这里,以后加配置项或改保存行为只改这一处。
955
+ * hooks = { onSaved, onError } 可省;省略时静默失败(与原先的 fire-and-forget 行为一致)。
956
+ */
957
+ function saveConfigPatch(patch, hooks) {
958
+ var h = hooks || {}
959
+ var p = apiPost(API.config, patch)
960
+ if (h.onSaved || h.onError) p.then(function (d) { if (h.onSaved) h.onSaved(d) }, function (e) { if (h.onError) h.onError(e) })
961
+ else p.catch(function () {})
962
+ return p
963
+ }
964
+ /**
965
+ * 语义引擎状态的统一刷新(2026-09-10 第 0 步止血):此前同一段 fetch + 状态写回在界面里逐字重复 10 处,
966
+ * 响应形状或失败兜底要改时得挨个改。apply 收合并后的状态对象(已带 loaded:true),onError 可省。
967
+ */
968
+ function refreshSem(apply, onError) {
969
+ try {
970
+ fetch(API.semanticStatus).then(function (r) { return r.json() }).then(function (j) { if (typeof apply === 'function') apply(Object.assign({ loaded: true }, j)) }, function (e) { if (typeof onError === 'function') onError(e) }).catch(function () {})
971
+ } catch (_) {}
972
+ }
911
973
 
912
974
  // ───────────────────────── 样式 ─────────────────────────
913
975
  // 视觉:液态玻璃(毛玻璃)—— backdrop-filter + DSH 主题令牌(--dsw-alias-*),
@@ -1478,7 +1540,7 @@ window.__ModuleLoader__.load({
1478
1540
  var st = p2.status === 'ok' ? t('pyWizOk') + (p2.isVenv ? ' · ' + t('pyWizVenvRec') : '') + (p2.version ? ' (' + p2.version + ')' : '') : p2.status === 'too-old' ? t('pyWizTooOld') + (p2.version ? ' (' + p2.version + ')' : '') : t('pyWizMissing')
1479
1541
  return h('div', { key: idx, style: { fontSize: 'calc(10.5px * var(--dam-scale))', opacity: p2.status === 'ok' ? 1 : 0.55, margin: '2px 0' } }, '· ' + p2.label + ' — ' + st)
1480
1542
  })
1481
- var allReady = data.venvOk && data.depsOk && data.modelReady
1543
+ var allReady = data.venvOk && data.depsOk && data.modelReady && data.configOk
1482
1544
  return h('div', { style: { border: '1px solid color-mix(in srgb, var(--dam-accent, #2456c4) 40%, transparent)', borderRadius: '10px', padding: '10px 12px', marginBottom: '8px', fontSize: 'calc(11.5px * var(--dam-scale))', lineHeight: 1.6 } },
1483
1545
  h('div', { style: { fontWeight: 700, marginBottom: '4px' } }, t('pyWizTitle')),
1484
1546
  h('div', { style: { opacity: .6, fontSize: 'calc(10.5px * var(--dam-scale))', marginBottom: '6px' } }, t('pyWizModelHint')),
@@ -1618,16 +1680,16 @@ window.__ModuleLoader__.load({
1618
1680
  var setFb = fbPair[1]
1619
1681
  useEffect(function () {
1620
1682
  var alive = true
1621
- fetch('/api/dsh-auto-memory/shadow-recent').then(function (r) { return r.json() }).then(function (j) {
1683
+ fetch(API.shadowRecent).then(function (r) { return r.json() }).then(function (j) {
1622
1684
  if (alive) setData((j && j.rows) || [])
1623
1685
  }).catch(function (e) { if (alive) setErr(String(e && e.message)) })
1624
- fetch('/api/dsh-auto-memory/review-feedback').then(function (r) { return r.json() }).then(function (j) {
1686
+ fetch(API.reviewFeedback).then(function (r) { return r.json() }).then(function (j) {
1625
1687
  if (alive) setFb(j || {})
1626
1688
  }).catch(function () {})
1627
1689
  return function () { alive = false }
1628
1690
  }, [])
1629
1691
  function send(obsId, choice) {
1630
- fetch('/api/dsh-auto-memory/review-feedback', { method: 'POST',
1692
+ fetch(API.reviewFeedback, { method: 'POST',
1631
1693
  headers: { 'Content-Type': 'application/json' },
1632
1694
  body: JSON.stringify({ observationId: obsId, choice: choice }) })
1633
1695
  .then(function (r) { return r.json() })
@@ -1796,7 +1858,7 @@ window.__ModuleLoader__.load({
1796
1858
  var setActMsg = actMsgPair[1]
1797
1859
  useEffect(function () {
1798
1860
  var alive = true
1799
- fetch('/api/dsh-auto-memory/memory-hub').then(function (r) { return r.json() }).then(function (j) {
1861
+ fetch(API.memoryHub).then(function (r) { return r.json() }).then(function (j) {
1800
1862
  if (alive) setData(j || null)
1801
1863
  }).catch(function (e) { if (alive) setErr(String(e && e.message)) })
1802
1864
  return function () { alive = false }
@@ -1804,7 +1866,7 @@ window.__ModuleLoader__.load({
1804
1866
  // M9 审批动作(G-02 同款 append-only 精神):晋升/激活/弃用走 /memory-hub POST,
1805
1867
  // 后端走 store 门槛判定,不绕过任何 gate;动作后刷新 overview。
1806
1868
  function hubAct(action, procedureId, v) {
1807
- fetch('/api/dsh-auto-memory/memory-hub', { method: 'POST',
1869
+ fetch(API.memoryHub, { method: 'POST',
1808
1870
  headers: { 'Content-Type': 'application/json' },
1809
1871
  body: JSON.stringify({ action: action, procedureId: procedureId, v: v }) })
1810
1872
  .then(function (r) { return r.json() })
@@ -1898,7 +1960,7 @@ window.__ModuleLoader__.load({
1898
1960
  var setDelFile = filePair[1]
1899
1961
  useEffect(function () {
1900
1962
  var alive = true
1901
- fetch('/api/dsh-auto-memory/storage-manage').then(function (r) { return r.json() }).then(function (j) {
1963
+ fetch(API.storageManage).then(function (r) { return r.json() }).then(function (j) {
1902
1964
  if (alive) setData(j || null)
1903
1965
  }).catch(function (e) { if (alive) setErr(String(e && e.message)) })
1904
1966
  return function () { alive = false }
@@ -1906,7 +1968,7 @@ window.__ModuleLoader__.load({
1906
1968
  function act(action, payload, onDone) {
1907
1969
  setMsg('')
1908
1970
  var body = Object.assign({ action: action }, payload || {})
1909
- fetch('/api/dsh-auto-memory/storage-manage', { method: 'POST',
1971
+ fetch(API.storageManage, { method: 'POST',
1910
1972
  headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
1911
1973
  .then(function (r) { return r.json() })
1912
1974
  .then(function (j) {
@@ -1916,7 +1978,7 @@ window.__ModuleLoader__.load({
1916
1978
  }
1917
1979
  setMsg(action + ': ' + reason)
1918
1980
  if (onDone) onDone(j)
1919
- var again = fetch('/api/dsh-auto-memory/storage-manage').then(function (r2) { return r2.json() })
1981
+ var again = fetch(API.storageManage).then(function (r2) { return r2.json() })
1920
1982
  again.then(function (j2) { setData(j2 || null) })
1921
1983
  })
1922
1984
  .catch(function (e) { setMsg(action + ' failed: ' + String(e && e.message)) })
@@ -2012,6 +2074,10 @@ window.__ModuleLoader__.load({
2012
2074
  // 流程:③刷新仪式(旧 Agent 先刷 PLAN+账本)→ 取材料 → 修A(workspaceId 建会话)→ 序号标题
2013
2075
  // → 修B(沿用模型+思考档位)→ 打开 → 注入材料。
2014
2076
  var continueFlowBusy = false
2077
+ // 刷新仪式认到的旧会话 id(2026-09-10):权限继承要用它反查旧会话预设。
2078
+ // 材料里的 prevSessionId 依赖宿主 _lastAgent,重启后可能为空;这份来自 handoff-state.refresh,
2079
+ // 宿主侧带磁盘回退,更可靠。
2080
+ var lastRefreshSessionId = ''
2015
2081
  function continueCreateArgs(d) {
2016
2082
  // 修A(2026-09-08):官方 session.create 只接受 workspaceId 或 cwd 之一(同传报 bad-request),
2017
2083
  // 且只有 workspaceId 分支会 workspace.attachSession()(api-session-controller/lib/index.js:566-588);
@@ -2021,18 +2087,31 @@ window.__ModuleLoader__.load({
2021
2087
  }
2022
2088
  async function refreshOldSession(onMsg) {
2023
2089
  // ③接续前刷新仪式:让旧 Agent 把 PLAN.md 与交接账本刷到最新,host 再用最新材料组装交接。
2024
- // 任何失败/超时都 fail-soft(用现有材料继续),绝不阻塞接续。
2090
+ // 2026-09-10:原先任何失败/超时都静默(fail-soft),用户看到的现象是"点了接续却什么也没发生、旧会话毫无动静"。
2091
+ // 现改为 fail-soft 但**可见** —— 跳过/失败/超时都回报一句原因,接续本身照常继续。
2025
2092
  try {
2026
2093
  var st = await apiGet(API.handoffState)
2027
2094
  var rf = st && st.refresh
2028
- if (!rf || !rf.sessionId || !rf.prompt) return { ok: false, skipped: true }
2029
- if (!remoteFace || !remoteFace.session || typeof remoteFace.session.prompt !== 'function') return { ok: false, skipped: true }
2095
+ if (!rf || !rf.sessionId || !rf.prompt) {
2096
+ if (onMsg) onMsg(locale === 'zh' ? '⚠ 刷新仪式已跳过:宿主未给出旧会话 id(刚重启、旧会话未活跃时会发生)——本次直接用现有材料接续。' : '⚠ Refresh ritual skipped: host supplied no old-session id (happens right after a restart) — continuing with current material.')
2097
+ return { ok: false, skipped: true }
2098
+ }
2099
+ if (!remoteFace || !remoteFace.session || typeof remoteFace.session.prompt !== 'function') {
2100
+ if (onMsg) onMsg(locale === 'zh' ? '⚠ 刷新仪式已跳过:harness 未提供 remote.session。' : '⚠ Refresh ritual skipped: remote.session unavailable.')
2101
+ return { ok: false, skipped: true }
2102
+ }
2030
2103
  var before = String((st.planMtime || 0)) + '|' + String((st.ledgers && st.ledgers[0] && st.ledgers[0].name) || '')
2104
+ lastRefreshSessionId = String(rf.sessionId || '')
2031
2105
  if (onMsg) onMsg(locale === 'zh' ? '刷新仪式:请旧 Agent 更新白板 PLAN 与交接账本…' : 'Refresh ritual: asking the old agent to update PLAN + ledger…')
2032
2106
  await remoteFace.session.prompt({ requestId: amReqId('amr'), sessionId: rf.sessionId, mode: 'queue', content: [{ type: 'text', text: rf.prompt }], clientTimeZone: amCtzValue() })
2033
2107
  var waited = await waitForRefresh(rf.sessionId, before)
2108
+ if (onMsg && waited !== 'updated') onMsg(locale === 'zh' ? '刷新仪式:旧会话未在超时内产出新白板/账本(' + waited + '),用现有材料继续。' : 'Refresh ritual: old session produced no new PLAN/ledger within timeout (' + waited + '); continuing with current material.')
2034
2109
  return { ok: true, waited: waited }
2035
- } catch (eRf) { return { ok: false, error: String(eRf && eRf.message ? eRf.message : eRf) } }
2110
+ } catch (eRf) {
2111
+ var msg = String(eRf && eRf.message ? eRf.message : eRf)
2112
+ if (onMsg) onMsg(locale === 'zh' ? '⚠ 刷新仪式失败:' + msg + '(用现有材料继续)' : '⚠ Refresh ritual failed: ' + msg + ' (continuing with current material)')
2113
+ return { ok: false, error: msg }
2114
+ }
2036
2115
  }
2037
2116
  async function waitForRefresh(oldId, before) {
2038
2117
  // 结束条件(任一):材料变了 / 旧会话跑完这一轮(true→false) / 超时(默认 90s,配置可调)。
@@ -2079,7 +2158,28 @@ window.__ModuleLoader__.load({
2079
2158
  try { await rf.session.selectModel({ sessionId: newId, provider: d.provider, model: d.model, reasoningEffort: d.reasoningEffort || undefined }) } catch (eMdl) {}
2080
2159
  }
2081
2160
  try { if (sessions && typeof sessions.open === 'function') sessions.open(newId) } catch (eOpen) {}
2161
+ // 权限继承(2026-09-10):官方 session.create 不收权限字段,新会话一律落 settings 的
2162
+ // permission.defaultPreset(本机默认曾=workspace-write+ask)→ 接续后每一步都要用户批准,静默运行被打断。
2163
+ // 两段式:投料前先试(命中则**首轮就已继承**),投料后再试(新会话的 agent 往往是投料时才建出来)。
2164
+ var fromSid = String((d && d.prevSessionId) || lastRefreshSessionId || '')
2165
+ var permPreset = ''
2166
+ if (fromSid) {
2167
+ try {
2168
+ var pp1 = await apiPost(API.handoffPermission, { fromSessionId: fromSid, toSessionId: newId, attempts: [0, 400] })
2169
+ if (pp1 && pp1.ok) permPreset = String(pp1.preset || '')
2170
+ } catch (ePp1) {}
2171
+ }
2082
2172
  await rf.session.prompt({ requestId: amReqId('amc'), sessionId: newId, mode: 'queue', content: [{ type: 'text', text: d.carryText }], clientTimeZone: amCtzValue() })
2173
+ if (!permPreset && fromSid) {
2174
+ try {
2175
+ var pp2 = await apiPost(API.handoffPermission, { fromSessionId: fromSid, toSessionId: newId, attempts: [0, 700, 1600] })
2176
+ if (pp2 && pp2.ok) permPreset = String(pp2.preset || '')
2177
+ else if (onMsg && pp2 && pp2.reason && pp2.reason !== 'no-permission-service') {
2178
+ onMsg(locale === 'zh' ? '⚠ 权限未继承(' + pp2.reason + '),新会话按设置里的默认权限运行。' : '⚠ Permission not inherited (' + pp2.reason + '); new session runs with the default permission.')
2179
+ }
2180
+ } catch (ePp2) {}
2181
+ }
2182
+ if (onMsg && permPreset) onMsg(locale === 'zh' ? '已继承旧会话权限:' + permPreset : 'Permission inherited: ' + permPreset)
2083
2183
  return newId
2084
2184
  }
2085
2185
  async function runContinueFlow(opts) {
@@ -2093,7 +2193,8 @@ window.__ModuleLoader__.load({
2093
2193
  var ritual = !(cfg && cfg.autoContinueRefreshRitual === false) && !(opts && opts.skipRefresh)
2094
2194
  if (ritual) { try { await refreshOldSession(onMsg) } catch (eRf) {} }
2095
2195
  onMsg(locale === 'zh' ? '正在构造交接材料(含旧会话转写)…' : 'Preparing handoff material (incl. prev-session transcript)…')
2096
- var d = await apiPost(API.handoffContinue, {})
2196
+ var fromSidForCarry = String(lastRefreshSessionId || currentSessionIdClient() || '')
2197
+ var d = await apiPost(API.handoffContinue, fromSidForCarry ? { fromSessionId: fromSidForCarry } : {})
2097
2198
  if (!d || !d.ok) throw new Error((d && d.error) || 'no handoff material')
2098
2199
  var newId = await executeContinue(d, onMsg)
2099
2200
  try { localStorage.setItem('dsh-auto-memory.autoCont.last', String(Date.now())) } catch (eL) {}
@@ -2133,7 +2234,7 @@ window.__ModuleLoader__.load({
2133
2234
  }, [])
2134
2235
  var autoSave = function (patch) {
2135
2236
  setAutoCfg(function (p) { return Object.assign({}, p, patch) })
2136
- apiPost(API.config, patch).catch(function () {})
2237
+ saveConfigPatch(patch)
2137
2238
  }
2138
2239
  async function oneClickContinue() {
2139
2240
  // 一键接续 = 共用执行器(刷新仪式 → 材料 → workspaceId 建会话 → 沿用模型 → 注入),与自动接续同一路径
@@ -3045,10 +3146,10 @@ window.__ModuleLoader__.load({
3045
3146
  if (!wizSt) {
3046
3147
  wizSt = { loaded: false }
3047
3148
  window['dsh-auto-memory.wizStatus'] = wizSt
3048
- fetch('/api/dsh-auto-memory/semantic-status').then(function (r) { return r.json() }).then(function (j) {
3049
- Object.assign(wizSt, j, { loaded: true })
3149
+ refreshSem(function (s) {
3150
+ Object.assign(wizSt, s)
3050
3151
  try { dlgTick() } catch (e9) {}
3051
- }).catch(function () { Object.assign(wizSt, { loaded: true, ready: false }) })
3152
+ }, function () { Object.assign(wizSt, { loaded: true, ready: false }) })
3052
3153
  }
3053
3154
  var tourDlPhase = wizSt && wizSt.download && wizSt.download.phase
3054
3155
  var tourShowing = !!dialogState && (dialogState.kind === 'welcomeTour' || dialogState.kind === 'modelDownload')
@@ -3058,19 +3159,19 @@ window.__ModuleLoader__.load({
3058
3159
  setTourStep(0)
3059
3160
  wizSt.tourCfg = null
3060
3161
  wizSt.cfgLoaded = false
3061
- fetch('/api/dsh-auto-memory/config').then(function (r) { return r.json() }).then(function (j) {
3162
+ fetch(API.config).then(function (r) { return r.json() }).then(function (j) {
3062
3163
  wizSt.tourCfg = (j && (j.config || j)) || {}
3063
3164
  // mode 型开关(唤起注入模式)实际存储在 embedding-config.json(semantic-emit 接口维护),
3064
3165
  // 需从 semantic-status 合并进来,导览开关才能显示真实当前值并与设置页联动。
3065
3166
  try {
3066
- fetch('/api/dsh-auto-memory/semantic-status').then(function (r2) { return r2.json() }).then(function (j2) {
3167
+ refreshSem(function (j2) {
3067
3168
  if (j2 && j2.activationEmitMode !== undefined) { wizSt.tourCfg.activationEmitMode = j2.activationEmitMode; try { dlgTick() } catch (e13) {} }
3068
- }).catch(function () {})
3169
+ })
3069
3170
  } catch (_) {}
3070
3171
  wizSt.cfgLoaded = true
3071
3172
  try { dlgTick() } catch (e11) {}
3072
3173
  }).catch(function () { wizSt.tourCfg = wizSt.tourCfg || {}; wizSt.cfgLoaded = true })
3073
- fetch('/api/dsh-auto-memory/external').then(function (r) { return r.json() }).then(function (j) {
3174
+ fetch(API.external).then(function (r) { return r.json() }).then(function (j) {
3074
3175
  wizSt.extScan = j
3075
3176
  try { dlgTick() } catch (e12) {}
3076
3177
  }).catch(function () {})
@@ -3080,9 +3181,9 @@ window.__ModuleLoader__.load({
3080
3181
  if (!tourShowing) return undefined
3081
3182
  if (wizSt.ready) return undefined // 已就绪无需轮询
3082
3183
  var iv = setInterval(function () {
3083
- fetch('/api/dsh-auto-memory/semantic-status').then(function (r) { return r.json() }).then(function (j) {
3184
+ refreshSem(function (j) {
3084
3185
  Object.assign(wizSt, j); try { dlgTick() } catch (e10) {}
3085
- }).catch(function () {})
3186
+ })
3086
3187
  }, 1500)
3087
3188
  return function () { clearInterval(iv) }
3088
3189
  }, [tourShowing, tourDlPhase, wizSt.ready])
@@ -3095,7 +3196,7 @@ window.__ModuleLoader__.load({
3095
3196
  if (wizSt.ready || wizSt.loaded === false) return undefined
3096
3197
  var ph = wizSt.download && wizSt.download.phase
3097
3198
  if (ph === 'downloading' || ph === 'verifying' || ph === 'done' || ph === 'error') return undefined
3098
- apiPost('/api/dsh-auto-memory/semantic-download', { action: 'start', mirror: 'auto' }).catch(function () {})
3199
+ apiPost(API.semanticDownload, { action: 'start', mirror: 'auto' }).catch(function () {})
3099
3200
  }, [tourShowing, tourStep, wizSt.ready])
3100
3201
  if (!dialogState) return null
3101
3202
  // 左下角小卡片(记忆按钮上方),高透明毛玻璃,不遮全屏
@@ -3213,7 +3314,7 @@ window.__ModuleLoader__.load({
3213
3314
  : t('dlDone'))
3214
3315
  : (locale === 'zh' ? '未下载 · 词法检索照常可用' : 'Not downloaded · lexical search keeps working')
3215
3316
  var startDl = function () {
3216
- apiPost('/api/dsh-auto-memory/semantic-download', { action: 'start', mirror: 'auto' }).catch(function () {})
3317
+ apiPost(API.semanticDownload, { action: 'start', mirror: 'auto' }).catch(function () {})
3217
3318
  }
3218
3319
  // 功能开关:读当前值(带每项默认)/点击即时写配置(乐观更新+POST,失败不回滚——下次重看向导会以真实配置为准)
3219
3320
  var EXT_SOURCE_KEYS = ['workbuddy-user', 'workbuddy-profile', 'codebuddy-memory', 'claude-global', 'project-conventions', 'workbuddy-sessions', 'claude-sessions', 'codex-sessions']
@@ -3239,9 +3340,9 @@ window.__ModuleLoader__.load({
3239
3340
  // mode 型开关(唤起注入模式)走 semantic-emit 接口——与设置页同源(embedding-config.json),
3240
3341
  // 保证导览切换与设置页双向联动;其余开关写常规 config。
3241
3342
  if (tg.mode) {
3242
- apiPost('/api/dsh-auto-memory/semantic-emit', { mode: patch[tg.key] }).catch(function () {})
3343
+ apiPost(API.semanticEmit, { mode: patch[tg.key] }).catch(function () {})
3243
3344
  } else {
3244
- apiPost('/api/dsh-auto-memory/config', patch).catch(function () {})
3345
+ saveConfigPatch(patch)
3245
3346
  }
3246
3347
  }
3247
3348
  // 外部来源单源勾选(扫描行;勾选即写 externalSources 对象)
@@ -3252,7 +3353,7 @@ window.__ModuleLoader__.load({
3252
3353
  cur[id] = (cur[id] !== false) ? false : true
3253
3354
  c.externalSources = cur
3254
3355
  dlgTick()
3255
- apiPost('/api/dsh-auto-memory/config', { externalSources: cur }).catch(function () {})
3356
+ saveConfigPatch({ externalSources: cur })
3256
3357
  }
3257
3358
  var tourExtOn = function (id) {
3258
3359
  var c = wizSt.tourCfg || {}
@@ -3538,8 +3639,11 @@ window.__ModuleLoader__.load({
3538
3639
  useEffect(function () {
3539
3640
  var alive = true
3540
3641
  var load = function () {
3642
+ // 带上当前会话 id:宿主据此只在「属于本窗口的」armed 上弹确认卡(2026-09-10)
3643
+ var sidQ = ''
3644
+ try { sidQ = String(currentSessionIdClient() || '') } catch (eS) {}
3541
3645
  apiGet(API.config).then(function (c) { if (alive) setAcCfg(c || {}) }).catch(function () {})
3542
- apiGet(API.autoContState).then(function (s) { if (alive && s) setAcState(s) }).catch(function () {})
3646
+ apiGet(API.autoContState, sidQ ? { sessionId: sidQ } : {}).then(function (s) { if (alive && s) setAcState(s) }).catch(function () {})
3543
3647
  }
3544
3648
  load()
3545
3649
  var iv = setInterval(load, 3000)
@@ -3554,21 +3658,34 @@ window.__ModuleLoader__.load({
3554
3658
  // 触发/倒计时/超时执行全部由宿主侧驱动(2.2.6),浏览器即使被节流也不影响接续。
3555
3659
  var poll = function () {
3556
3660
  if (!alive) return
3557
- apiGet(API.autoContState).then(function (s) {
3661
+ var sidQ = ''
3662
+ try { sidQ = String(currentSessionIdClient() || '') } catch (eS) {}
3663
+ apiGet(API.autoContState, sidQ ? { sessionId: sidQ } : {}).then(function (s) {
3558
3664
  if (!alive || !s) return
3559
3665
  setAcState(s)
3560
3666
  if (s.executing) {
3667
+ // 2026-09-10:执行中/已完成时必须**收起确认卡** —— 旧实现在这两个分支直接 return,
3668
+ // acConfirm 保持旧值,于是宿主已接续完成后,卡片仍停在「同意接续/拒绝」上
3669
+ // (实测:旧窗口一直显示确认卡 + 「✓ 已自动接续到新会话」并存,像是「它还想接续」)。
3670
+ setAcConfirm(null)
3671
+ setAcCd(0)
3561
3672
  setAcSt(locale === 'zh' ? '宿主正在自动接续:刷新白板/账本 → 建新会话 → 注入材料…' : 'Host auto-continuing: refresh PLAN/ledger → new session → inject…')
3562
3673
  return
3563
3674
  }
3564
- if (s.lastOk) {
3675
+ var okAt = Number(s.lastOk && s.lastOk.at) || 0
3676
+ // 10 分钟有效期(2026-09-10):宿主状态是全局单值,提示不设期限会一直挂在每个窗口上
3677
+ if (s.lastOk && (okAt === 0 || Date.now() - okAt < 10 * 60 * 1000)) {
3678
+ setAcConfirm(null)
3679
+ setAcCd(0)
3565
3680
  setAcSt((locale === 'zh' ? '✓ 已自动接续到新会话,可直接上手' : '✓ Auto-continued into a new session') + (s.lastOk.model ? ' · ' + s.lastOk.model + (s.lastOk.reasoningEffort ? '/' + s.lastOk.reasoningEffort : '') : ''))
3566
3681
  return
3567
3682
  }
3568
3683
  if (s.error) { setAcSt('✗ ' + s.error); return }
3569
3684
  var arm = s.armed
3570
3685
  if (arm && arm.ratio >= thr) {
3571
- setAcConfirm({ ratio: Number(arm.ratio) || 0, tokens: Number(arm.tokens) || 0, window: Number(arm.window) || 0, edgeAt: Number(arm.edgeAt) || 0 })
3686
+ // ring/wall 必须一起带进来:卡片读的是 acConfirm.ring / acConfirm.wall,
3687
+ // 宿主透出了但这里不拷 → 双口径那行永远不渲染(2026-09-10 自查发现)
3688
+ setAcConfirm({ ratio: Number(arm.ratio) || 0, tokens: Number(arm.tokens) || 0, window: Number(arm.window) || 0, ring: Number(arm.ring) || 0, wall: Number(arm.wall) || 0, edgeAt: Number(arm.edgeAt) || 0 })
3572
3689
  setAcCd(Math.max(0, Math.ceil((Number(arm.expiresAt) || 0) - Date.now()) / 1000))
3573
3690
  } else {
3574
3691
  setAcConfirm(null)
@@ -3609,7 +3726,9 @@ window.__ModuleLoader__.load({
3609
3726
  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)' } },
3610
3727
  acConfirm ? h('div', { 'data-dam-autocont-confirm': '' }, [
3611
3728
  h('div', { style: { marginBottom: '4px' } }, t('autoContConfirm').replace('{p}', String(Math.round((acConfirm.ratio || 0) * 100)))),
3612
- h('div', { 'data-dam-hint': '' }, (acConfirm.tokens || 0).toLocaleString() + ' / ' + (acConfirm.window || 0).toLocaleString() + ' token'),
3729
+ h('div', { 'data-dam-hint': '' }, (acConfirm.tokens || 0).toLocaleString() + ' / ' + (acConfirm.window || 0).toLocaleString() + ' token'
3730
+ + (acConfirm.ring > 0 ? ' · ' + (locale === 'zh' ? '官方小圈读数 ' : 'ring ') + String(Math.round((acConfirm.ring || 0) * 100)) + '%' : '')
3731
+ + (acConfirm.wall > 0 ? ' · ' + (locale === 'zh' ? '距硬墙还剩 ' : 'to the wall ') + Math.max(0, (acConfirm.wall || 0) - (acConfirm.tokens || 0)).toLocaleString() + ' token' : '')),
3613
3732
  h('div', { style: { marginTop: '8px' } },
3614
3733
  h('button', { 'data-dam-btn': '', onClick: onAgree }, t('autoContAgree')),
3615
3734
  h('button', { 'data-dam-btn': '', style: { marginLeft: '6px' }, onClick: onReject }, t('autoContReject')),
@@ -3737,6 +3856,29 @@ window.__ModuleLoader__.load({
3737
3856
  }
3738
3857
  kids.push(h('input', { key: '__manual__', 'data-dam-input': '', style: { marginTop: '6px', width: '100%' }, value: cfg.subagentModel || '', placeholder: locale === 'zh' ? '手动输入(可选,provider 跟随路由默认)' : 'manual entry (optional, provider follows routing default)', onChange: function (e) { set('subagentModel', String(e.target.value || '').trim()); set('subagentProvider', '') } }))
3739
3858
  }
3859
+ // 思考强度(DSH 0.1.5 agentOptions.reasoningEffort):与模型成对下发给子代理;留空=跟随模型默认。
3860
+ // 合法值按 DeepSeek 适配器口径 off|low|high|max(默认 high)——服务端同样做白名单过滤。
3861
+ var effortOpts = [
3862
+ ['', locale === 'zh' ? '跟随默认' : 'default'],
3863
+ ['off', 'off'],
3864
+ ['low', 'low'],
3865
+ ['high', 'high'],
3866
+ ['max', 'max'],
3867
+ ]
3868
+ var curEffort = String(cfg.subagentReasoningEffort || '').toLowerCase()
3869
+ kids.push(h('div', { key: '__effort__', style: { marginTop: '10px', paddingTop: '8px', borderTop: '1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.25)) 45%, transparent)' } },
3870
+ h('b', { style: { display: 'block', fontSize: 'calc(12px * var(--dam-scale))' } }, locale === 'zh' ? '思考强度(子代理)' : 'Reasoning effort (subagents)'),
3871
+ h('div', { 'data-dam-row': '', style: { flexWrap: 'wrap', marginTop: '4px' } },
3872
+ effortOpts.map(function (o) {
3873
+ return h('button', {
3874
+ key: 'eff-' + (o[0] || 'def'), 'data-dam-btn': '',
3875
+ style: { padding: '3px 9px', fontWeight: curEffort === o[0] ? 700 : 400, opacity: curEffort === o[0] ? 1 : 0.72 },
3876
+ onClick: function () { set('subagentReasoningEffort', o[0]) },
3877
+ }, o[1] + (curEffort === o[0] ? ' ✓' : ''))
3878
+ })),
3879
+ h('div', { 'data-dam-hint': '' }, locale === 'zh'
3880
+ ? 'off=关闭思维链,low/high/max 递增思考深度(DeepSeek 默认 high)。只作用于本插件的子代理(时段总结/问候/自动沉淀/蒸馏),不影响主对话;留空或模型不支持时跟随模型默认。'
3881
+ : 'off disables thinking; low/high/max increase depth (DeepSeek default: high). Applies only to this plugin’s subagents, never the main conversation; empty follows the model default.')))
3740
3882
  return h('div', { style: panelStyle }, kids)
3741
3883
  }
3742
3884
  var verPair = useState(null)
@@ -3785,9 +3927,7 @@ window.__ModuleLoader__.load({
3785
3927
  var detBusy = detBusyPair[0]
3786
3928
  var setDetBusy = detBusyPair[1]
3787
3929
  useEffect(function () {
3788
- fetch('/api/dsh-auto-memory/semantic-status').then(function (r) { return r.json() }).then(function (j) {
3789
- setSem(Object.assign({ loaded: true }, j))
3790
- }).catch(function () { setSem({ loaded: true, ready: false }) })
3930
+ refreshSem(setSem, function () { setSem({ loaded: true, ready: false }) })
3791
3931
  return function () {}
3792
3932
  }, [])
3793
3933
  // 下载进行中每 1.5s 轮询真实进度(服务端流式记账 bytesDone/bytesTotal/mirrorUsed)
@@ -3795,9 +3935,7 @@ window.__ModuleLoader__.load({
3795
3935
  var ph = sem && sem.download && sem.download.phase
3796
3936
  if (ph !== 'downloading' && ph !== 'verifying') return function () {}
3797
3937
  var iv = setInterval(function () {
3798
- fetch('/api/dsh-auto-memory/semantic-status').then(function (r) { return r.json() }).then(function (j) {
3799
- setSem(Object.assign({ loaded: true }, j))
3800
- }).catch(function () {})
3938
+ refreshSem(setSem)
3801
3939
  }, 1500)
3802
3940
  return function () { clearInterval(iv) }
3803
3941
  }, [sem && sem.download && sem.download.phase])
@@ -3821,8 +3959,10 @@ window.__ModuleLoader__.load({
3821
3959
  function save() {
3822
3960
  if (busy) return
3823
3961
  setBusy(true); setMsg(''); setErr('')
3824
- apiPost(API.config, cfg).then(function (d) { setCfg(d.config); setDirty(false); setMsg(t('saved') + (d.migrated ? ' ' + d.migrated : '')); setBusy(false); if (d && d.config && d.config.locale) applyLocalePref(d.config.locale); try { fetch('/api/dsh-auto-memory/semantic-status').then(function (r2) { return r2.json() }).then(function (j2) { setSem(Object.assign({ loaded: true }, j2)) }).catch(function () {}) } catch (_) {} })
3825
- .catch(function (e) { setErr(e.message); setBusy(false) })
3962
+ saveConfigPatch(cfg, {
3963
+ onSaved: function (d) { setCfg(d.config); setDirty(false); setMsg(t('saved') + (d.migrated ? ' ' + d.migrated : '')); setBusy(false); if (d && d.config && d.config.locale) applyLocalePref(d.config.locale); refreshSem(setSem) },
3964
+ onError: function (e) { setErr(e.message); setBusy(false) }
3965
+ })
3826
3966
  }
3827
3967
  function field(label, control, hint) {
3828
3968
  return h('div', { 'data-dam-settings-row': '' },
@@ -3854,7 +3994,7 @@ window.__ModuleLoader__.load({
3854
3994
  else if ((rec === 'download-model' || rec === 'install-peer' || rec === 'setup-both') && mode !== 'python') setGuide('js')
3855
3995
  else if (rec === 'setup-both' && mode === 'python') setGuide('python')
3856
3996
  }).catch(function () { setDet({ failed: true }); setDetBusy(false) })
3857
- fetch('/api/dsh-auto-memory/semantic-status').then(function (r2) { return r2.json() }).then(function (j2) { setSem(Object.assign({ loaded: true }, j2)) }).catch(function () {})
3997
+ refreshSem(setSem)
3858
3998
  }
3859
3999
  function onEngineModeChange(e) {
3860
4000
  var v = e.target.value
@@ -3874,12 +4014,11 @@ window.__ModuleLoader__.load({
3874
4014
  // 2026-08-27 修复显示不跟随:切换后重新 fetch semantic-status,刷新「当前生效检索」
3875
4015
  // (sem.resolvedTier 原只在挂载/下载时更新,切换后不刷新导致一直显示旧档位)。
3876
4016
  try {
3877
- fetch('/api/dsh-auto-memory/semantic-status').then(function (r2) { return r2.json() }).then(function (j2) {
3878
- var s2 = Object.assign({ loaded: true }, j2)
4017
+ refreshSem(function (s2) {
3879
4018
  setSem(s2)
3880
4019
  if (v === 'js' && s2.ready === false) setGuide('js')
3881
4020
  else if (v === 'python' && s2.pythonInt8Present === false) setGuide('python')
3882
- }).catch(function () {})
4021
+ })
3883
4022
  } catch (_) {}
3884
4023
  }
3885
4024
  var sectionLabels = {
@@ -3906,7 +4045,7 @@ window.__ModuleLoader__.load({
3906
4045
  field(t('fJsCooldown'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 60, value: cfg.jsDecideCooldownRounds === undefined ? 1 : cfg.jsDecideCooldownRounds, onChange: function (e) { set('jsDecideCooldownRounds', (function () { var v = Number(e.target.value); return (Number.isFinite(v) && v >= 0) ? v : 1 })()) /* #19: 0=不冷却 是合法值,|| 1 会吃掉 0 */ } }), t('fJsCooldownHint')),
3907
4046
  field(t('fJsDelta'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 1, step: 0.005, value: cfg.jsDecideDeltaExp === undefined ? 0.01 : cfg.jsDecideDeltaExp, onChange: function (e) { var v = Number(e.target.value); set('jsDecideDeltaExp', Number.isFinite(v) && v >= 0 ? v : 0.01) } }), t('fJsDeltaHint')),
3908
4047
  field(t('fJsExcerpt'), h('input', { 'data-dam-input': '', type: 'number', min: 20, max: 480, value: cfg.jsDecideExcerptChars === undefined ? 40 : cfg.jsDecideExcerptChars, onChange: function (e) { set('jsDecideExcerptChars', Math.max(20, Math.min(480, Number(e.target.value) || 40))) } }), t('fJsExcerptHint')),
3909
- field(t('fEmitMode'), h('select', { 'data-dam-select': '', value: (sem && sem.activationEmitMode) || 'shadow', onChange: function (e) { var m = e.target.value; apiPost('/api/dsh-auto-memory/semantic-emit', { mode: m }).then(function () { try { fetch('/api/dsh-auto-memory/semantic-status').then(function (r2) { return r2.json() }).then(function (j2) { setSem(Object.assign({ loaded: true }, j2)) }).catch(function () {}) } catch (_) {} }).catch(function () {}) } },
4048
+ field(t('fEmitMode'), h('select', { 'data-dam-select': '', value: (sem && sem.activationEmitMode) || 'shadow', onChange: function (e) { var m = e.target.value; apiPost(API.semanticEmit, { mode: m }).then(function () { refreshSem(setSem) }).catch(function () {}) } },
3910
4049
  h('option', { value: 'shadow' }, locale === 'zh' ? 'shadow 只记录' : 'shadow (record only)'),
3911
4050
  h('option', { value: 'canary-explicit' }, locale === 'zh' ? 'canary 显式回忆注入' : 'canary (explicit recall)'),
3912
4051
  h('option', { value: 'active' }, locale === 'zh' ? 'active 全部注入' : 'active (all)')), t('fEmitModeHint')),
@@ -3922,6 +4061,10 @@ window.__ModuleLoader__.load({
3922
4061
  h('option', { value: 'js' }, t('semJs')),
3923
4062
  h('option', { value: 'python' }, t('semPy'))),
3924
4063
  h('button', { 'data-dam-btn': '', title: locale === 'zh' ? '自动检测本机语义引擎环境(JS/Python 资产+推理库),并联动安装助手' : 'Auto-detect local semantic engine assets & runtime, with setup assistant', disabled: detBusy, onClick: function () { void runDetect() } }, detBusy ? '⏳' : '⟳ 检测'),
4064
+ // 2026-09-10:向导入口原先只在"资产未就绪"时自动弹出,已装好的用户反而进不去(想重装/换布局无处可点)。
4065
+ // 现在常驻一个显式开关,永远可达;已就绪时打开也只是复用同一套下载/校验步骤。
4066
+ h('button', { 'data-dam-btn': '', title: locale === 'zh' ? '打开/收起安装向导(按当前引擎选择:JS 模型 或 Python 引擎)' : 'Open/collapse setup wizard (follows the selected engine)', onClick: function () { setGuide(guide ? '' : ((cfg.semanticEngineMode === 'python') ? 'python' : 'js')) } },
4067
+ guide ? (locale === 'zh' ? '收起向导' : 'Hide wizard') : (locale === 'zh' ? '🧩 安装向导' : '🧩 Setup')),
3925
4068
  ]), t('semModeHint')),
3926
4069
  sem.loaded ? h('div', { 'data-dam-hint': '', style: { marginTop: '-4px', marginBottom: '8px' } },
3927
4070
  t('semResolved') + ': ' + (sem.resolvedTier === 'c2' ? t('tierC2') + ' ✓' : sem.resolvedTier === 'c3' ? t('tierC3') + ' ✓' : t('tierC1')))
@@ -4005,13 +4148,13 @@ window.__ModuleLoader__.load({
4005
4148
  : (isJs && dl.phase === 'done' && !ready ? t('dlDone') + (locale === 'zh' ? '(缺运行库时需安装 @huggingface/transformers)' : ' (install @huggingface/transformers if runtime missing)')
4006
4149
  : (locale === 'zh' ? '下载进度' : 'Download progress'))))
4007
4150
  var refreshSem = function () {
4008
- fetch('/api/dsh-auto-memory/semantic-status').then(function (r2) { return r2.json() }).then(function (j2) { setSem(Object.assign({ loaded: true }, j2)) }).catch(function () {})
4151
+ refreshSem(setSem)
4009
4152
  }
4010
4153
  var startDl = function () {
4011
- apiPost('/api/dsh-auto-memory/semantic-download', { action: 'start', mirror: mirror }).then(refreshSem).catch(function () {})
4154
+ apiPost(API.semanticDownload, { action: 'start', mirror: mirror }).then(refreshSem).catch(function () {})
4012
4155
  }
4013
4156
  var cancelDl = function () {
4014
- apiPost('/api/dsh-auto-memory/semantic-download', { action: 'cancel', mirror: mirror }).then(refreshSem).catch(function () {})
4157
+ apiPost(API.semanticDownload, { action: 'cancel', mirror: mirror }).then(refreshSem).catch(function () {})
4015
4158
  }
4016
4159
  return h('div', { style: { border: '1px solid color-mix(in srgb, var(--dam-accent, #2456c4) 40%, transparent)', borderRadius: '10px', padding: '10px 12px', marginBottom: '8px', fontSize: 'calc(11.5px * var(--dam-scale))', lineHeight: 1.6 } },
4017
4160
  h('div', { 'data-dam-row': '', style: { alignItems: 'center' } },
@@ -4103,6 +4246,8 @@ window.__ModuleLoader__.load({
4103
4246
  : null]),
4104
4247
  section('injection', sectionLabels.injection, [field(t('fInject'), h('input', { type: 'checkbox', checked: !!cfg.injectEnabled, onChange: function (e) { set('injectEnabled', e.target.checked) } }), t('fInjectHint')),
4105
4248
  field(t('fBudget'), h('input', { 'data-dam-input': '', type: 'number', min: 400, value: cfg.injectBudgetChars, onChange: function (e) { set('injectBudgetChars', Number(e.target.value) || 2400) } }), t('fBudgetHint')),
4249
+ field(t('fNoteCap'), h('input', { 'data-dam-input': '', type: 'number', min: 500, value: cfg.noteCapacityChars === undefined ? 12000 : cfg.noteCapacityChars, onChange: function (e) { set('noteCapacityChars', Number(e.target.value) || 12000) } }), t('fNoteCapHint')),
4250
+ field(t('fUserCap'), h('input', { 'data-dam-input': '', type: 'number', min: 500, value: cfg.userCapacityChars === undefined ? 12000 : cfg.userCapacityChars, onChange: function (e) { set('userCapacityChars', Number(e.target.value) || 12000) } }), t('fUserCapHint')),
4106
4251
  field(t('fDays'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 14, value: cfg.recentDaysInjected, onChange: function (e) { set('recentDaysInjected', Number(e.target.value) || 1) } }), t('fDaysHint')),
4107
4252
  field(t('fExtBudget'), h('input', { 'data-dam-input': '', type: 'number', min: 200, value: cfg.externalInjectionChars === undefined ? 1400 : cfg.externalInjectionChars, onChange: function (e) { set('externalInjectionChars', Number(e.target.value) || 1400) } }), t('fExtBudgetHint')),
4108
4253
  field(t('fSnapGap'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 50, value: cfg.snapshotMinGapRounds === undefined ? 5 : cfg.snapshotMinGapRounds, onChange: function (e) { set('snapshotMinGapRounds', Number(e.target.value) || 5) } }), t('fSnapGapHint')),
@@ -4136,10 +4281,10 @@ window.__ModuleLoader__.load({
4136
4281
  field(t('fMaintScheduleTime'), h('input', { 'data-dam-input': '', placeholder: '10:00', value: cfg.maintainScheduleTime === undefined ? '10:00' : cfg.maintainScheduleTime, onChange: function (e) { set('maintainScheduleTime', String(e.target.value || '').trim()) } }), t('fMaintScheduleTimeHint')),
4137
4282
  field(t('fStyle'), h('select', { 'data-dam-select': '', value: cfg.reflectStyle, onChange: function (e) { set('reflectStyle', e.target.value) } },
4138
4283
  STYLE_IDS.map(function (id) { return h('option', { key: id, value: id }, t('style' + id.charAt(0).toUpperCase() + id.slice(1))) })), t('fStyleHint')),
4139
- field(locale === 'zh' ? '总结/问候默认模型' : 'Summary & greeting model', h('div', { 'data-dam-row': '', style: { flex: 1 } },
4140
- h('span', { style: { flex: 1, fontSize: 'calc(12px * var(--dam-scale))', wordBreak: 'break-all', opacity: cfg.subagentModel ? 1 : 0.6 } }, cfg.subagentModel || (locale === 'zh' ? '跟随路由默认' : 'routing default')),
4141
- h('button', { 'data-dam-btn': '', onClick: openModels }, locale === 'zh' ? '选择模型' : 'Pick model')),
4142
- locale === 'zh' ? '用于时段总结、问候语、自动沉淀等 subagent 功能;从检测到的模型中选择,或留空跟随路由默认。保存后生效。' : 'For scheduled summaries, greetings and auto-consolidation subagents; pick a detected model or leave empty for the routing default. Applies after saving.'),
4284
+ field(locale === 'zh' ? '子代理模型 / 思考强度' : 'Subagent model & reasoning effort', h('div', { 'data-dam-row': '', style: { flex: 1 } },
4285
+ h('span', { style: { flex: 1, fontSize: 'calc(12px * var(--dam-scale))', wordBreak: 'break-all', opacity: cfg.subagentModel ? 1 : 0.6 } }, (cfg.subagentModel || (locale === 'zh' ? '跟随路由默认' : 'routing default')) + (String(cfg.subagentReasoningEffort || '') ? ' · ' + String(cfg.subagentReasoningEffort) : '')),
4286
+ h('button', { 'data-dam-btn': '', onClick: openModels }, locale === 'zh' ? '选择模型 / 强度' : 'Pick model / effort')),
4287
+ locale === 'zh' ? '用于时段总结、问候语、自动沉淀、蒸馏等 subagent 功能;模型与思考强度(off/low/high/max)都经 DSH 的 agentOptions 下发,留空跟随路由与模型默认。保存后生效。' : 'For scheduled summaries, greetings, auto-consolidation and distillation subagents; model and reasoning effort (off/low/high/max) are delivered via DSH agentOptions. Empty follows routing and model defaults. Applies after saving.'),
4143
4288
  mdlOpen ? buildModelDrawer() : null,
4144
4289
  ]),
4145
4290
  section('context', sectionLabels.context, [field(t('fHandoff'), h('input', { type: 'checkbox', checked: cfg.handoffEnabled !== false, onChange: function (e) { set('handoffEnabled', e.target.checked) } }), t('fHandoffHint')),