@a9i5k4/dsh-auto-memory 2.4.0 → 2.4.2

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.2 fingerprint: drawer-summary-ok')
11
11
  window.__ModuleLoader__.load({
12
12
  id: '@a9i5k4/dsh-auto-memory',
13
13
  factory: (require) => {
@@ -284,7 +284,7 @@ window.__ModuleLoader__.load({
284
284
  fMaintSchedule: '定时 30 天蒸馏', fMaintScheduleHint: '每天到点自动把超过 30 天的旧日志蒸馏归档(等价 memory_maintain);无旧日志时零成本跳过。',
285
285
  fMaintScheduleTime: '蒸馏触发时间', fMaintScheduleTimeHint: 'HH:MM,默认 10:00(与固化时间错开)。',
286
286
  waterCardTitle: '上下文水位', waterAutoSrc: '自动检测', waterManualSrc: '手动设定', waterOfficialSrc: '官方路由容量', waterFallbackSrc: '回退默认值', waterNotMeasured: '本会话尚未测量(发送一轮后自动更新)', waterMeterOfficial: '官方计量', waterMeterHeuristic: '启发式降级', waterThresholdHint: '水位达到 {t} 时注入交接建议并自动补写账本;计量优先用官方 token-meter(与聊天框 context ring 同源),不可用时降级启发式估算;窗口优先取官方路由容量(request/context 的 contextWindow),其次按当前模型查 settings.yaml contextWindow,也可在下方手动覆盖。',
287
- 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: '已跳过本次接续(下一个轮次边界再提醒)',
288
288
  continueCardTitle: '一键接续', continueCardHint: '写好交接账本→创建新会话→把交接材料作为首条消息预载,并自动切换到新会话继续任务(接续材料含白板节选+最新账本;当前工作区无账本时自动取全局最近一篇)。', continueBtn: '一键接续到新会话', continueBusy: '处理中…',
289
289
  fStyle: '反思风格', fStyleHint: '生活化 / 专业性 / 由内容决定。',
290
290
  fLocale: '界面语言', fLocaleHint: '默认跟随 DSH 系统语言;也可手动指定中文 / English。', followSystem: '跟随系统语言',
@@ -567,6 +567,37 @@ window.__ModuleLoader__.load({
567
567
 
568
568
  // ───────────────────────── 更新弹窗 / 首次指导 ─────────────────────────
569
569
  var CHANGELOG = {
570
+ '2.4.2': { zh: [
571
+ '变更:自动接续改为**出厂默认关闭**(老用户已落盘的显式设置不受影响)。原因:现有水位判据把「预留输出」从分母里扣掉,实测在上下文刚过半时就触发接续,与官方「约 80% 才压缩」相差约 45%,对不熟悉的人就是白烧 token —— 先默认关,等判据改成与官方压缩同一坐标系后再评估翻回。',
572
+ '想用的人可以在「白板 / 接续」页签里手动打开(开关一直没动,只是默认值变了)。',
573
+ ], en: [
574
+ 'Change: auto-continue is now OFF by default (explicit values already saved by existing users are unaffected). Reason: the current water-level ratio subtracts the route\'s reserved output from the denominator, so it fired when the context was barely half full — about 45% earlier than the official compaction point (~80%). Defaulting off stops burning tokens until the ratio is aligned with the official coordinate system.',
575
+ 'You can still turn it on yourself from the Whiteboard / Handoff tab; only the default changed.',
576
+ ] },
577
+ '2.4.1': { zh: [
578
+ '修复:已接续过的会话会被再次触发接续(重复建窗口)。新增「已接续闩锁」并落盘(session id 为键、重启有效):宿主路径在交接材料投料成功后落闩,浏览器路径在新会话真建成后落闩,失败不落闩以便重试。',
579
+ '修复:宿主已完成接续后,确认卡仍停在「同意接续/拒绝」并与「已自动接续到新会话」并存;arm 状态改为按会话过滤,不再在其他窗口弹「本会话水位已达」。',
580
+ '修复:水位双口径那一行(本会话水位 / 官方小圈读数 / 距硬墙余量)此前因漏拷贝 ring/wall 永不渲染。',
581
+ '内部:界面路径表引入唯一前缀常量与一致性锁(客户端路径必须都在宿主表内、表外零裸字面量);写配置 4 条路径收敛为一个出口;10 处重复的语义状态刷新收敛为一处。',
582
+ '流程:自本版起,发版必须同步 CHANGELOG、应用内更新说明与版本标识(release.mjs 已加校验,缺一即拒绝构建)。',
583
+ ], en: [
584
+ '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.',
585
+ '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".',
586
+ '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.',
587
+ '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.',
588
+ 'Process: from this version on, releases must update CHANGELOG, in-app release notes and version identifiers together (release.mjs now refuses to build otherwise).',
589
+ ] },
590
+ '2.4.0': { zh: [
591
+ '★ 水位判据按「该会话当前模型」的预留额度自适应:消息可用上限 = 窗口 − 该路由的 maxTokens(实测 384,000 预留 → 可用 616,000),阈值随之落在健康区间;压缩与 CONTEXT_WINDOW_EXCEEDED 升为硬触发。',
592
+ '★ 修复自动接续静默失效:DSH 0.1.5 起 requestId 必填,宿主兜底接续漏传该字段时官方抛错被包装成误导性的 "prompt rejected" —— 现象是新会话建出来了、交接材料却从未送达。',
593
+ '★ 记忆容量口径重做:改为「文件容量上限」(noteCapacityChars / userCapacityChars,默认各 12000 字符),先 AI 折叠成要点、失败退回整条归档,写入不再被堵死。',
594
+ '★ Python 语义引擎贯通:安装向导写入正确的工作目录、模型落位与 onnxFile 契约对齐、向导常驻设置页,装了就能用。',
595
+ ], en: [
596
+ '★ 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.',
597
+ '★ 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.',
598
+ '★ 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.',
599
+ '★ 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.',
600
+ ] },
570
601
  '2.3.0': { zh: [
571
602
  '★ 检索改为分层语义唤回:memory_recall 先返回 L0 摘要列表(每条记忆约 90 字,含 id/得分/匹配原因),需要原文再按 id 展开——此前全文直接进 embedding,超过模型 512 token 上限的条目尾部全部丢失(实测最长一条 9,822 字符);现在摘要永远在限制内,长记忆不再有损。',
572
603
  '★ 检索融合改为三臂 rank-space:词法 + 语义 + 时间 三路各自排名后按 RRF(k=60)融合,取代旧的 minmax 归一化加权——旧方案分数只反映「这一批里排第几」,候选集一换分数就漂移、候选只剩 1 条时退化为常数,而「要不要注入」的决策恰恰依赖分数与阈值比较。现在排序与决策解耦,分数不再说谎。',
@@ -855,45 +886,56 @@ window.__ModuleLoader__.load({
855
886
  }
856
887
 
857
888
  // ───────────────────────── API ─────────────────────────
889
+ // 路由前缀唯一常量(2026-09-10 第 0 步止血):表中每条路径由它拼出,不再逐个重复全串。
890
+ var ROUTE_PREFIX = '/api/dsh-auto-memory'
891
+ // 宿主半边(lib/index.js 的 API)声明的是同一批路径 —— 两个半边是独立 bundle(本文件经 __ModuleLoader__ 手写加载,
892
+ // 无法 import 宿主模块),所以"只有一份"这件事由 tests/smoke/ 下的路径表一致性锁保证:
893
+ // 客户端路径 ⊆ 宿主路径、宿主独有路径只允许显式白名单、本文件表外不得再出现裸路径字面量。
858
894
  var API = {
859
- state: '/api/dsh-auto-memory/state',
860
- list: '/api/dsh-auto-memory/list',
861
- file: '/api/dsh-auto-memory/file',
862
- recall: '/api/dsh-auto-memory/recall',
863
- smartRecall: '/api/dsh-auto-memory/smart-recall',
864
- workspaces: '/api/dsh-auto-memory/workspaces',
865
- debug: '/api/dsh-auto-memory/debug',
866
- scanDirty: '/api/dsh-auto-memory/scan-dirty',
867
- browseDir: '/api/dsh-auto-memory/browse-dir',
868
- pickDir: '/api/dsh-auto-memory/pick-dir',
869
- updateCheck: '/api/dsh-auto-memory/update-check',
870
- update: '/api/dsh-auto-memory/update',
871
- config: '/api/dsh-auto-memory/config',
872
- reflect: '/api/dsh-auto-memory/reflect',
873
- reflectAuto: '/api/dsh-auto-memory/reflect-auto',
874
- note: '/api/dsh-auto-memory/note',
875
- external: '/api/dsh-auto-memory/external',
876
- externalView: '/api/dsh-auto-memory/external-view',
877
- externalRemove: '/api/dsh-auto-memory/external-remove',
878
- externalImport: '/api/dsh-auto-memory/external-import',
879
- calendar: '/api/dsh-auto-memory/calendar',
880
- summarize: '/api/dsh-auto-memory/summarize',
881
- greet: '/api/dsh-auto-memory/greet',
882
- notices: '/api/dsh-auto-memory/notices',
883
- models: '/api/dsh-auto-memory/models',
884
- semanticDeepDetect: '/api/dsh-auto-memory/semantic-deep-detect',
885
- pyDetect: '/api/dsh-auto-memory/python-setup/detect',
886
- pyVenv: '/api/dsh-auto-memory/python-setup/venv',
887
- pyDeps: '/api/dsh-auto-memory/python-setup/deps',
888
- pyModel: '/api/dsh-auto-memory/python-setup/model',
889
- pyCancel: '/api/dsh-auto-memory/python-setup/cancel',
890
- pyStatus: '/api/dsh-auto-memory/python-setup/status',
891
- handoffState: '/api/dsh-auto-memory/handoff-state',
892
- handoffContinue: '/api/dsh-auto-memory/handoff-continue',
893
- handoffPermission: '/api/dsh-auto-memory/handoff-permission',
894
- autoContState: '/api/dsh-auto-memory/auto-continue-state',
895
- autoContDecide: '/api/dsh-auto-memory/auto-continue-decide',
896
- semanticDownload: '/api/dsh-auto-memory/semantic-download',
895
+ state: ROUTE_PREFIX + '/state',
896
+ list: ROUTE_PREFIX + '/list',
897
+ file: ROUTE_PREFIX + '/file',
898
+ recall: ROUTE_PREFIX + '/recall',
899
+ smartRecall: ROUTE_PREFIX + '/smart-recall',
900
+ workspaces: ROUTE_PREFIX + '/workspaces',
901
+ debug: ROUTE_PREFIX + '/debug',
902
+ scanDirty: ROUTE_PREFIX + '/scan-dirty',
903
+ browseDir: ROUTE_PREFIX + '/browse-dir',
904
+ pickDir: ROUTE_PREFIX + '/pick-dir',
905
+ updateCheck: ROUTE_PREFIX + '/update-check',
906
+ update: ROUTE_PREFIX + '/update',
907
+ config: ROUTE_PREFIX + '/config',
908
+ reflect: ROUTE_PREFIX + '/reflect',
909
+ reflectAuto: ROUTE_PREFIX + '/reflect-auto',
910
+ note: ROUTE_PREFIX + '/note',
911
+ external: ROUTE_PREFIX + '/external',
912
+ externalView: ROUTE_PREFIX + '/external-view',
913
+ externalRemove: ROUTE_PREFIX + '/external-remove',
914
+ externalImport: ROUTE_PREFIX + '/external-import',
915
+ calendar: ROUTE_PREFIX + '/calendar',
916
+ summarize: ROUTE_PREFIX + '/summarize',
917
+ greet: ROUTE_PREFIX + '/greet',
918
+ notices: ROUTE_PREFIX + '/notices',
919
+ models: ROUTE_PREFIX + '/models',
920
+ semanticDeepDetect: ROUTE_PREFIX + '/semantic-deep-detect',
921
+ semanticStatus: ROUTE_PREFIX + '/semantic-status',
922
+ semanticEmit: ROUTE_PREFIX + '/semantic-emit',
923
+ pyDetect: ROUTE_PREFIX + '/python-setup/detect',
924
+ pyVenv: ROUTE_PREFIX + '/python-setup/venv',
925
+ pyDeps: ROUTE_PREFIX + '/python-setup/deps',
926
+ pyModel: ROUTE_PREFIX + '/python-setup/model',
927
+ pyCancel: ROUTE_PREFIX + '/python-setup/cancel',
928
+ pyStatus: ROUTE_PREFIX + '/python-setup/status',
929
+ handoffState: ROUTE_PREFIX + '/handoff-state',
930
+ handoffContinue: ROUTE_PREFIX + '/handoff-continue',
931
+ handoffPermission: ROUTE_PREFIX + '/handoff-permission',
932
+ autoContState: ROUTE_PREFIX + '/auto-continue-state',
933
+ autoContDecide: ROUTE_PREFIX + '/auto-continue-decide',
934
+ semanticDownload: ROUTE_PREFIX + '/semantic-download',
935
+ shadowRecent: ROUTE_PREFIX + '/shadow-recent',
936
+ reviewFeedback: ROUTE_PREFIX + '/review-feedback',
937
+ memoryHub: ROUTE_PREFIX + '/memory-hub',
938
+ storageManage: ROUTE_PREFIX + '/storage-manage',
897
939
  }
898
940
  function query(params) {
899
941
  var search = new URLSearchParams()
@@ -913,6 +955,28 @@ window.__ModuleLoader__.load({
913
955
  if (!res.ok) throw new Error(body.error || ('POST ' + path + ' → HTTP ' + res.status))
914
956
  return body
915
957
  }
958
+ /**
959
+ * 写配置的唯一出口(2026-09-10 第 0 步止血):此前有 4 条互不相同的写配置路径
960
+ * (设置页整对象 POST / 接续页 patch / 向导两处 patch),端点、后置刷新、错误处理各写一遍。
961
+ * 现在全部经这里,以后加配置项或改保存行为只改这一处。
962
+ * hooks = { onSaved, onError } 可省;省略时静默失败(与原先的 fire-and-forget 行为一致)。
963
+ */
964
+ function saveConfigPatch(patch, hooks) {
965
+ var h = hooks || {}
966
+ var p = apiPost(API.config, patch)
967
+ if (h.onSaved || h.onError) p.then(function (d) { if (h.onSaved) h.onSaved(d) }, function (e) { if (h.onError) h.onError(e) })
968
+ else p.catch(function () {})
969
+ return p
970
+ }
971
+ /**
972
+ * 语义引擎状态的统一刷新(2026-09-10 第 0 步止血):此前同一段 fetch + 状态写回在界面里逐字重复 10 处,
973
+ * 响应形状或失败兜底要改时得挨个改。apply 收合并后的状态对象(已带 loaded:true),onError 可省。
974
+ */
975
+ function refreshSem(apply, onError) {
976
+ try {
977
+ 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 () {})
978
+ } catch (_) {}
979
+ }
916
980
 
917
981
  // ───────────────────────── 样式 ─────────────────────────
918
982
  // 视觉:液态玻璃(毛玻璃)—— backdrop-filter + DSH 主题令牌(--dsw-alias-*),
@@ -1623,16 +1687,16 @@ window.__ModuleLoader__.load({
1623
1687
  var setFb = fbPair[1]
1624
1688
  useEffect(function () {
1625
1689
  var alive = true
1626
- fetch('/api/dsh-auto-memory/shadow-recent').then(function (r) { return r.json() }).then(function (j) {
1690
+ fetch(API.shadowRecent).then(function (r) { return r.json() }).then(function (j) {
1627
1691
  if (alive) setData((j && j.rows) || [])
1628
1692
  }).catch(function (e) { if (alive) setErr(String(e && e.message)) })
1629
- fetch('/api/dsh-auto-memory/review-feedback').then(function (r) { return r.json() }).then(function (j) {
1693
+ fetch(API.reviewFeedback).then(function (r) { return r.json() }).then(function (j) {
1630
1694
  if (alive) setFb(j || {})
1631
1695
  }).catch(function () {})
1632
1696
  return function () { alive = false }
1633
1697
  }, [])
1634
1698
  function send(obsId, choice) {
1635
- fetch('/api/dsh-auto-memory/review-feedback', { method: 'POST',
1699
+ fetch(API.reviewFeedback, { method: 'POST',
1636
1700
  headers: { 'Content-Type': 'application/json' },
1637
1701
  body: JSON.stringify({ observationId: obsId, choice: choice }) })
1638
1702
  .then(function (r) { return r.json() })
@@ -1801,7 +1865,7 @@ window.__ModuleLoader__.load({
1801
1865
  var setActMsg = actMsgPair[1]
1802
1866
  useEffect(function () {
1803
1867
  var alive = true
1804
- fetch('/api/dsh-auto-memory/memory-hub').then(function (r) { return r.json() }).then(function (j) {
1868
+ fetch(API.memoryHub).then(function (r) { return r.json() }).then(function (j) {
1805
1869
  if (alive) setData(j || null)
1806
1870
  }).catch(function (e) { if (alive) setErr(String(e && e.message)) })
1807
1871
  return function () { alive = false }
@@ -1809,7 +1873,7 @@ window.__ModuleLoader__.load({
1809
1873
  // M9 审批动作(G-02 同款 append-only 精神):晋升/激活/弃用走 /memory-hub POST,
1810
1874
  // 后端走 store 门槛判定,不绕过任何 gate;动作后刷新 overview。
1811
1875
  function hubAct(action, procedureId, v) {
1812
- fetch('/api/dsh-auto-memory/memory-hub', { method: 'POST',
1876
+ fetch(API.memoryHub, { method: 'POST',
1813
1877
  headers: { 'Content-Type': 'application/json' },
1814
1878
  body: JSON.stringify({ action: action, procedureId: procedureId, v: v }) })
1815
1879
  .then(function (r) { return r.json() })
@@ -1903,7 +1967,7 @@ window.__ModuleLoader__.load({
1903
1967
  var setDelFile = filePair[1]
1904
1968
  useEffect(function () {
1905
1969
  var alive = true
1906
- fetch('/api/dsh-auto-memory/storage-manage').then(function (r) { return r.json() }).then(function (j) {
1970
+ fetch(API.storageManage).then(function (r) { return r.json() }).then(function (j) {
1907
1971
  if (alive) setData(j || null)
1908
1972
  }).catch(function (e) { if (alive) setErr(String(e && e.message)) })
1909
1973
  return function () { alive = false }
@@ -1911,7 +1975,7 @@ window.__ModuleLoader__.load({
1911
1975
  function act(action, payload, onDone) {
1912
1976
  setMsg('')
1913
1977
  var body = Object.assign({ action: action }, payload || {})
1914
- fetch('/api/dsh-auto-memory/storage-manage', { method: 'POST',
1978
+ fetch(API.storageManage, { method: 'POST',
1915
1979
  headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
1916
1980
  .then(function (r) { return r.json() })
1917
1981
  .then(function (j) {
@@ -1921,7 +1985,7 @@ window.__ModuleLoader__.load({
1921
1985
  }
1922
1986
  setMsg(action + ': ' + reason)
1923
1987
  if (onDone) onDone(j)
1924
- var again = fetch('/api/dsh-auto-memory/storage-manage').then(function (r2) { return r2.json() })
1988
+ var again = fetch(API.storageManage).then(function (r2) { return r2.json() })
1925
1989
  again.then(function (j2) { setData(j2 || null) })
1926
1990
  })
1927
1991
  .catch(function (e) { setMsg(action + ' failed: ' + String(e && e.message)) })
@@ -2177,7 +2241,7 @@ window.__ModuleLoader__.load({
2177
2241
  }, [])
2178
2242
  var autoSave = function (patch) {
2179
2243
  setAutoCfg(function (p) { return Object.assign({}, p, patch) })
2180
- apiPost(API.config, patch).catch(function () {})
2244
+ saveConfigPatch(patch)
2181
2245
  }
2182
2246
  async function oneClickContinue() {
2183
2247
  // 一键接续 = 共用执行器(刷新仪式 → 材料 → workspaceId 建会话 → 沿用模型 → 注入),与自动接续同一路径
@@ -3089,10 +3153,10 @@ window.__ModuleLoader__.load({
3089
3153
  if (!wizSt) {
3090
3154
  wizSt = { loaded: false }
3091
3155
  window['dsh-auto-memory.wizStatus'] = wizSt
3092
- fetch('/api/dsh-auto-memory/semantic-status').then(function (r) { return r.json() }).then(function (j) {
3093
- Object.assign(wizSt, j, { loaded: true })
3156
+ refreshSem(function (s) {
3157
+ Object.assign(wizSt, s)
3094
3158
  try { dlgTick() } catch (e9) {}
3095
- }).catch(function () { Object.assign(wizSt, { loaded: true, ready: false }) })
3159
+ }, function () { Object.assign(wizSt, { loaded: true, ready: false }) })
3096
3160
  }
3097
3161
  var tourDlPhase = wizSt && wizSt.download && wizSt.download.phase
3098
3162
  var tourShowing = !!dialogState && (dialogState.kind === 'welcomeTour' || dialogState.kind === 'modelDownload')
@@ -3102,19 +3166,19 @@ window.__ModuleLoader__.load({
3102
3166
  setTourStep(0)
3103
3167
  wizSt.tourCfg = null
3104
3168
  wizSt.cfgLoaded = false
3105
- fetch('/api/dsh-auto-memory/config').then(function (r) { return r.json() }).then(function (j) {
3169
+ fetch(API.config).then(function (r) { return r.json() }).then(function (j) {
3106
3170
  wizSt.tourCfg = (j && (j.config || j)) || {}
3107
3171
  // mode 型开关(唤起注入模式)实际存储在 embedding-config.json(semantic-emit 接口维护),
3108
3172
  // 需从 semantic-status 合并进来,导览开关才能显示真实当前值并与设置页联动。
3109
3173
  try {
3110
- fetch('/api/dsh-auto-memory/semantic-status').then(function (r2) { return r2.json() }).then(function (j2) {
3174
+ refreshSem(function (j2) {
3111
3175
  if (j2 && j2.activationEmitMode !== undefined) { wizSt.tourCfg.activationEmitMode = j2.activationEmitMode; try { dlgTick() } catch (e13) {} }
3112
- }).catch(function () {})
3176
+ })
3113
3177
  } catch (_) {}
3114
3178
  wizSt.cfgLoaded = true
3115
3179
  try { dlgTick() } catch (e11) {}
3116
3180
  }).catch(function () { wizSt.tourCfg = wizSt.tourCfg || {}; wizSt.cfgLoaded = true })
3117
- fetch('/api/dsh-auto-memory/external').then(function (r) { return r.json() }).then(function (j) {
3181
+ fetch(API.external).then(function (r) { return r.json() }).then(function (j) {
3118
3182
  wizSt.extScan = j
3119
3183
  try { dlgTick() } catch (e12) {}
3120
3184
  }).catch(function () {})
@@ -3124,9 +3188,9 @@ window.__ModuleLoader__.load({
3124
3188
  if (!tourShowing) return undefined
3125
3189
  if (wizSt.ready) return undefined // 已就绪无需轮询
3126
3190
  var iv = setInterval(function () {
3127
- fetch('/api/dsh-auto-memory/semantic-status').then(function (r) { return r.json() }).then(function (j) {
3191
+ refreshSem(function (j) {
3128
3192
  Object.assign(wizSt, j); try { dlgTick() } catch (e10) {}
3129
- }).catch(function () {})
3193
+ })
3130
3194
  }, 1500)
3131
3195
  return function () { clearInterval(iv) }
3132
3196
  }, [tourShowing, tourDlPhase, wizSt.ready])
@@ -3139,7 +3203,7 @@ window.__ModuleLoader__.load({
3139
3203
  if (wizSt.ready || wizSt.loaded === false) return undefined
3140
3204
  var ph = wizSt.download && wizSt.download.phase
3141
3205
  if (ph === 'downloading' || ph === 'verifying' || ph === 'done' || ph === 'error') return undefined
3142
- apiPost('/api/dsh-auto-memory/semantic-download', { action: 'start', mirror: 'auto' }).catch(function () {})
3206
+ apiPost(API.semanticDownload, { action: 'start', mirror: 'auto' }).catch(function () {})
3143
3207
  }, [tourShowing, tourStep, wizSt.ready])
3144
3208
  if (!dialogState) return null
3145
3209
  // 左下角小卡片(记忆按钮上方),高透明毛玻璃,不遮全屏
@@ -3257,7 +3321,7 @@ window.__ModuleLoader__.load({
3257
3321
  : t('dlDone'))
3258
3322
  : (locale === 'zh' ? '未下载 · 词法检索照常可用' : 'Not downloaded · lexical search keeps working')
3259
3323
  var startDl = function () {
3260
- apiPost('/api/dsh-auto-memory/semantic-download', { action: 'start', mirror: 'auto' }).catch(function () {})
3324
+ apiPost(API.semanticDownload, { action: 'start', mirror: 'auto' }).catch(function () {})
3261
3325
  }
3262
3326
  // 功能开关:读当前值(带每项默认)/点击即时写配置(乐观更新+POST,失败不回滚——下次重看向导会以真实配置为准)
3263
3327
  var EXT_SOURCE_KEYS = ['workbuddy-user', 'workbuddy-profile', 'codebuddy-memory', 'claude-global', 'project-conventions', 'workbuddy-sessions', 'claude-sessions', 'codex-sessions']
@@ -3283,9 +3347,9 @@ window.__ModuleLoader__.load({
3283
3347
  // mode 型开关(唤起注入模式)走 semantic-emit 接口——与设置页同源(embedding-config.json),
3284
3348
  // 保证导览切换与设置页双向联动;其余开关写常规 config。
3285
3349
  if (tg.mode) {
3286
- apiPost('/api/dsh-auto-memory/semantic-emit', { mode: patch[tg.key] }).catch(function () {})
3350
+ apiPost(API.semanticEmit, { mode: patch[tg.key] }).catch(function () {})
3287
3351
  } else {
3288
- apiPost('/api/dsh-auto-memory/config', patch).catch(function () {})
3352
+ saveConfigPatch(patch)
3289
3353
  }
3290
3354
  }
3291
3355
  // 外部来源单源勾选(扫描行;勾选即写 externalSources 对象)
@@ -3296,7 +3360,7 @@ window.__ModuleLoader__.load({
3296
3360
  cur[id] = (cur[id] !== false) ? false : true
3297
3361
  c.externalSources = cur
3298
3362
  dlgTick()
3299
- apiPost('/api/dsh-auto-memory/config', { externalSources: cur }).catch(function () {})
3363
+ saveConfigPatch({ externalSources: cur })
3300
3364
  }
3301
3365
  var tourExtOn = function (id) {
3302
3366
  var c = wizSt.tourCfg || {}
@@ -3582,8 +3646,11 @@ window.__ModuleLoader__.load({
3582
3646
  useEffect(function () {
3583
3647
  var alive = true
3584
3648
  var load = function () {
3649
+ // 带上当前会话 id:宿主据此只在「属于本窗口的」armed 上弹确认卡(2026-09-10)
3650
+ var sidQ = ''
3651
+ try { sidQ = String(currentSessionIdClient() || '') } catch (eS) {}
3585
3652
  apiGet(API.config).then(function (c) { if (alive) setAcCfg(c || {}) }).catch(function () {})
3586
- apiGet(API.autoContState).then(function (s) { if (alive && s) setAcState(s) }).catch(function () {})
3653
+ apiGet(API.autoContState, sidQ ? { sessionId: sidQ } : {}).then(function (s) { if (alive && s) setAcState(s) }).catch(function () {})
3587
3654
  }
3588
3655
  load()
3589
3656
  var iv = setInterval(load, 3000)
@@ -3598,21 +3665,34 @@ window.__ModuleLoader__.load({
3598
3665
  // 触发/倒计时/超时执行全部由宿主侧驱动(2.2.6),浏览器即使被节流也不影响接续。
3599
3666
  var poll = function () {
3600
3667
  if (!alive) return
3601
- apiGet(API.autoContState).then(function (s) {
3668
+ var sidQ = ''
3669
+ try { sidQ = String(currentSessionIdClient() || '') } catch (eS) {}
3670
+ apiGet(API.autoContState, sidQ ? { sessionId: sidQ } : {}).then(function (s) {
3602
3671
  if (!alive || !s) return
3603
3672
  setAcState(s)
3604
3673
  if (s.executing) {
3674
+ // 2026-09-10:执行中/已完成时必须**收起确认卡** —— 旧实现在这两个分支直接 return,
3675
+ // acConfirm 保持旧值,于是宿主已接续完成后,卡片仍停在「同意接续/拒绝」上
3676
+ // (实测:旧窗口一直显示确认卡 + 「✓ 已自动接续到新会话」并存,像是「它还想接续」)。
3677
+ setAcConfirm(null)
3678
+ setAcCd(0)
3605
3679
  setAcSt(locale === 'zh' ? '宿主正在自动接续:刷新白板/账本 → 建新会话 → 注入材料…' : 'Host auto-continuing: refresh PLAN/ledger → new session → inject…')
3606
3680
  return
3607
3681
  }
3608
- if (s.lastOk) {
3682
+ var okAt = Number(s.lastOk && s.lastOk.at) || 0
3683
+ // 10 分钟有效期(2026-09-10):宿主状态是全局单值,提示不设期限会一直挂在每个窗口上
3684
+ if (s.lastOk && (okAt === 0 || Date.now() - okAt < 10 * 60 * 1000)) {
3685
+ setAcConfirm(null)
3686
+ setAcCd(0)
3609
3687
  setAcSt((locale === 'zh' ? '✓ 已自动接续到新会话,可直接上手' : '✓ Auto-continued into a new session') + (s.lastOk.model ? ' · ' + s.lastOk.model + (s.lastOk.reasoningEffort ? '/' + s.lastOk.reasoningEffort : '') : ''))
3610
3688
  return
3611
3689
  }
3612
3690
  if (s.error) { setAcSt('✗ ' + s.error); return }
3613
3691
  var arm = s.armed
3614
3692
  if (arm && arm.ratio >= thr) {
3615
- setAcConfirm({ ratio: Number(arm.ratio) || 0, tokens: Number(arm.tokens) || 0, window: Number(arm.window) || 0, edgeAt: Number(arm.edgeAt) || 0 })
3693
+ // ring/wall 必须一起带进来:卡片读的是 acConfirm.ring / acConfirm.wall,
3694
+ // 宿主透出了但这里不拷 → 双口径那行永远不渲染(2026-09-10 自查发现)
3695
+ 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 })
3616
3696
  setAcCd(Math.max(0, Math.ceil((Number(arm.expiresAt) || 0) - Date.now()) / 1000))
3617
3697
  } else {
3618
3698
  setAcConfirm(null)
@@ -3653,7 +3733,9 @@ window.__ModuleLoader__.load({
3653
3733
  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)' } },
3654
3734
  acConfirm ? h('div', { 'data-dam-autocont-confirm': '' }, [
3655
3735
  h('div', { style: { marginBottom: '4px' } }, t('autoContConfirm').replace('{p}', String(Math.round((acConfirm.ratio || 0) * 100)))),
3656
- h('div', { 'data-dam-hint': '' }, (acConfirm.tokens || 0).toLocaleString() + ' / ' + (acConfirm.window || 0).toLocaleString() + ' token'),
3736
+ h('div', { 'data-dam-hint': '' }, (acConfirm.tokens || 0).toLocaleString() + ' / ' + (acConfirm.window || 0).toLocaleString() + ' token'
3737
+ + (acConfirm.ring > 0 ? ' · ' + (locale === 'zh' ? '官方小圈读数 ' : 'ring ') + String(Math.round((acConfirm.ring || 0) * 100)) + '%' : '')
3738
+ + (acConfirm.wall > 0 ? ' · ' + (locale === 'zh' ? '距硬墙还剩 ' : 'to the wall ') + Math.max(0, (acConfirm.wall || 0) - (acConfirm.tokens || 0)).toLocaleString() + ' token' : '')),
3657
3739
  h('div', { style: { marginTop: '8px' } },
3658
3740
  h('button', { 'data-dam-btn': '', onClick: onAgree }, t('autoContAgree')),
3659
3741
  h('button', { 'data-dam-btn': '', style: { marginLeft: '6px' }, onClick: onReject }, t('autoContReject')),
@@ -3852,9 +3934,7 @@ window.__ModuleLoader__.load({
3852
3934
  var detBusy = detBusyPair[0]
3853
3935
  var setDetBusy = detBusyPair[1]
3854
3936
  useEffect(function () {
3855
- fetch('/api/dsh-auto-memory/semantic-status').then(function (r) { return r.json() }).then(function (j) {
3856
- setSem(Object.assign({ loaded: true }, j))
3857
- }).catch(function () { setSem({ loaded: true, ready: false }) })
3937
+ refreshSem(setSem, function () { setSem({ loaded: true, ready: false }) })
3858
3938
  return function () {}
3859
3939
  }, [])
3860
3940
  // 下载进行中每 1.5s 轮询真实进度(服务端流式记账 bytesDone/bytesTotal/mirrorUsed)
@@ -3862,9 +3942,7 @@ window.__ModuleLoader__.load({
3862
3942
  var ph = sem && sem.download && sem.download.phase
3863
3943
  if (ph !== 'downloading' && ph !== 'verifying') return function () {}
3864
3944
  var iv = setInterval(function () {
3865
- fetch('/api/dsh-auto-memory/semantic-status').then(function (r) { return r.json() }).then(function (j) {
3866
- setSem(Object.assign({ loaded: true }, j))
3867
- }).catch(function () {})
3945
+ refreshSem(setSem)
3868
3946
  }, 1500)
3869
3947
  return function () { clearInterval(iv) }
3870
3948
  }, [sem && sem.download && sem.download.phase])
@@ -3888,8 +3966,10 @@ window.__ModuleLoader__.load({
3888
3966
  function save() {
3889
3967
  if (busy) return
3890
3968
  setBusy(true); setMsg(''); setErr('')
3891
- 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 (_) {} })
3892
- .catch(function (e) { setErr(e.message); setBusy(false) })
3969
+ saveConfigPatch(cfg, {
3970
+ 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) },
3971
+ onError: function (e) { setErr(e.message); setBusy(false) }
3972
+ })
3893
3973
  }
3894
3974
  function field(label, control, hint) {
3895
3975
  return h('div', { 'data-dam-settings-row': '' },
@@ -3921,7 +4001,7 @@ window.__ModuleLoader__.load({
3921
4001
  else if ((rec === 'download-model' || rec === 'install-peer' || rec === 'setup-both') && mode !== 'python') setGuide('js')
3922
4002
  else if (rec === 'setup-both' && mode === 'python') setGuide('python')
3923
4003
  }).catch(function () { setDet({ failed: true }); setDetBusy(false) })
3924
- fetch('/api/dsh-auto-memory/semantic-status').then(function (r2) { return r2.json() }).then(function (j2) { setSem(Object.assign({ loaded: true }, j2)) }).catch(function () {})
4004
+ refreshSem(setSem)
3925
4005
  }
3926
4006
  function onEngineModeChange(e) {
3927
4007
  var v = e.target.value
@@ -3941,12 +4021,11 @@ window.__ModuleLoader__.load({
3941
4021
  // 2026-08-27 修复显示不跟随:切换后重新 fetch semantic-status,刷新「当前生效检索」
3942
4022
  // (sem.resolvedTier 原只在挂载/下载时更新,切换后不刷新导致一直显示旧档位)。
3943
4023
  try {
3944
- fetch('/api/dsh-auto-memory/semantic-status').then(function (r2) { return r2.json() }).then(function (j2) {
3945
- var s2 = Object.assign({ loaded: true }, j2)
4024
+ refreshSem(function (s2) {
3946
4025
  setSem(s2)
3947
4026
  if (v === 'js' && s2.ready === false) setGuide('js')
3948
4027
  else if (v === 'python' && s2.pythonInt8Present === false) setGuide('python')
3949
- }).catch(function () {})
4028
+ })
3950
4029
  } catch (_) {}
3951
4030
  }
3952
4031
  var sectionLabels = {
@@ -3973,7 +4052,7 @@ window.__ModuleLoader__.load({
3973
4052
  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')),
3974
4053
  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')),
3975
4054
  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')),
3976
- 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 () {}) } },
4055
+ 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 () {}) } },
3977
4056
  h('option', { value: 'shadow' }, locale === 'zh' ? 'shadow 只记录' : 'shadow (record only)'),
3978
4057
  h('option', { value: 'canary-explicit' }, locale === 'zh' ? 'canary 显式回忆注入' : 'canary (explicit recall)'),
3979
4058
  h('option', { value: 'active' }, locale === 'zh' ? 'active 全部注入' : 'active (all)')), t('fEmitModeHint')),
@@ -4076,13 +4155,13 @@ window.__ModuleLoader__.load({
4076
4155
  : (isJs && dl.phase === 'done' && !ready ? t('dlDone') + (locale === 'zh' ? '(缺运行库时需安装 @huggingface/transformers)' : ' (install @huggingface/transformers if runtime missing)')
4077
4156
  : (locale === 'zh' ? '下载进度' : 'Download progress'))))
4078
4157
  var refreshSem = function () {
4079
- fetch('/api/dsh-auto-memory/semantic-status').then(function (r2) { return r2.json() }).then(function (j2) { setSem(Object.assign({ loaded: true }, j2)) }).catch(function () {})
4158
+ refreshSem(setSem)
4080
4159
  }
4081
4160
  var startDl = function () {
4082
- apiPost('/api/dsh-auto-memory/semantic-download', { action: 'start', mirror: mirror }).then(refreshSem).catch(function () {})
4161
+ apiPost(API.semanticDownload, { action: 'start', mirror: mirror }).then(refreshSem).catch(function () {})
4083
4162
  }
4084
4163
  var cancelDl = function () {
4085
- apiPost('/api/dsh-auto-memory/semantic-download', { action: 'cancel', mirror: mirror }).then(refreshSem).catch(function () {})
4164
+ apiPost(API.semanticDownload, { action: 'cancel', mirror: mirror }).then(refreshSem).catch(function () {})
4086
4165
  }
4087
4166
  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 } },
4088
4167
  h('div', { 'data-dam-row': '', style: { alignItems: 'center' } },