@a9i5k4/dsh-auto-memory 3.1.5 → 3.1.7
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/docs/CONTINUITY-FLOW.md +6 -6
- package/docs/HANDBOOK.md +54 -53
- package/docs/M-CM-PLAN.md +1 -1
- package/docs/M-CM-STATE.md +1 -1
- package/docs/M-CM7-HANDOFF-LAYERED-RETRIEVAL.md +1 -1
- package/docs/M3B-CONTRACT.md +7 -7
- package/docs/M7-AUTONOMOUS-STATE.md +1 -1
- package/docs/M7-TASKSET-DISPATCH.md +1 -1
- package/docs/PROMPT-PACK-LAYERED-RECALL.md +3 -3
- package/docs/PROMPT-SET-STRICT.md +3 -3
- package/docs/RELEASE-GO-NOGO.md +1 -1
- package/docs/STATUS-BOARD.md +1 -1
- package/docs/USER-GUIDE.en.md +21 -21
- package/docs/USER-GUIDE.zh-CN.md +21 -21
- package/docs/WHITEPAPER.md +1 -1
- package/docs/prompts/FIX-AGENT-P12-FULL-REGRESSION.md +1 -1
- package/docs/prompts/LIVE-VERIFY-ZCODE.md +2 -2
- package/docs/prompts/P2-semantic-recall.md +2 -2
- package/docs/prompts/P7-write-fix.md +1 -1
- package/docs/prompts/RELEASE-AGENT.md +4 -4
- package/docs/prompts/ZCODE-DROPIN.md +1 -1
- package/lib/acceptance.js +6 -6
- package/lib/activation-host.js +15 -14
- package/lib/activation-inbox-state.js +4 -4
- package/lib/activation-inbox.js +44 -44
- package/lib/board-mode.js +3 -3
- package/lib/client.js +225 -41
- package/lib/config-io.js +12 -12
- package/lib/context-bridge.js +22 -22
- package/lib/context-host.js +24 -23
- package/lib/datadir.js +95 -0
- package/lib/degrade.js +21 -21
- package/lib/dsh-home.js +8 -8
- package/lib/engine-identity.js +12 -12
- package/lib/engine-switch.js +3 -3
- package/lib/episodic-store.js +13 -13
- package/lib/evidence-agg.js +4 -4
- package/lib/evidence-store.js +12 -12
- package/lib/fact-store.js +47 -47
- package/lib/handoff-anchor.js +4 -4
- package/lib/hub-io.js +356 -3
- package/lib/index-sync.js +5 -5
- package/lib/index.js +426 -242
- package/lib/intent-clean-safe.js +3 -3
- package/lib/intent-clean.js +1 -1
- package/lib/l0-extract.js +15 -15
- package/lib/l0-index-sync.js +8 -8
- package/lib/l0-index.js +11 -11
- package/lib/ledger-criteria.js +8 -8
- package/lib/m4-corpus.js +1 -1
- package/lib/m7-index-sync-host.js +2 -2
- package/lib/m7-wire.js +24 -24
- package/lib/memory-anchor.js +1 -1
- package/lib/memory-envelope.js +15 -15
- package/lib/memory-hub.js +95 -50
- package/lib/memory-importance.js +7 -7
- package/lib/memory-mutation.js +5 -5
- package/lib/note-status-apply.js +2 -2
- package/lib/note-status.js +7 -7
- package/lib/policies/activation_policy_v2.json +2 -2
- package/lib/policies/recall_intent_lr_v1.json +1 -1
- package/lib/procedure-observation.js +2 -2
- package/lib/procedure-store.js +117 -28
- package/lib/python-setup.js +6 -5
- package/lib/python-sidecar-client.js +26 -26
- package/lib/recall-fusion.js +12 -12
- package/lib/recall-stats.js +36 -1
- package/lib/rerank-host.js +11 -11
- package/lib/rules-edit.js +9 -9
- package/lib/rules-layer.js +26 -26
- package/lib/semantic-decide.js +7 -7
- package/lib/semantic-js.js +13 -13
- package/lib/shadow-host.js +7 -6
- package/lib/shadow-retrieval.js +40 -40
- package/lib/skill-export-host.js +7 -7
- package/lib/skill-export.js +6 -6
- package/lib/state-commit.js +10 -10
- package/lib/storage-manage.js +6 -6
- package/lib/tier-layer-inject.js +28 -28
- package/lib/tier0-catalog.js +4 -4
- package/lib/wb-contract.js +45 -45
- package/lib/wb-sidecar.js +11 -11
- package/package.json +3 -4
- package/python/m7_activation_features_v2.py +7 -7
- package/python/m7_embedding_v1.py +12 -12
- package/python/policies/activation_policy_v2.json +2 -2
- package/python/policies/decision-record-activation-v2-delta-exp-override-20260824.json +1 -1
- package/python/policies/decision-record-reasoning-kind-admission-20260826.json +2 -2
- package/python/policies/decision-record-stale-gate-per-candidate-20260825.json +2 -2
- package/python/policies/recall_intent_lr_v1.json +1 -1
- package/python/verify_policy_artifact.py +3 -3
- package/python/worker_semantic_v1.py +18 -18
- package/python/worker_v1.py +18 -18
package/lib/client.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* 3. settings.section — 自动记忆设置页(存储位置、注入、反思风格)
|
|
8
8
|
* Data flows over /api/dsh-auto-memory/* (loopback-only host routes).
|
|
9
9
|
*/
|
|
10
|
-
console.log('[dsh-auto-memory] client v3.1.
|
|
10
|
+
console.log('[dsh-auto-memory] client v3.1.7 fingerprint: three-reported-bugs-fix')
|
|
11
11
|
window.__ModuleLoader__.load({
|
|
12
12
|
id: '@a9i5k4/dsh-auto-memory',
|
|
13
13
|
factory: (require) => {
|
|
@@ -265,7 +265,7 @@ window.__ModuleLoader__.load({
|
|
|
265
265
|
fReasoning: '思维链监听', fReasoningHint: '把模型思维链纳入实时观测(重启后生效)。默认开——闭源模型的概括式思维链同样纳入。',
|
|
266
266
|
fChildObs: '分支会话观测', fChildObsHint: '跨天续接的会话会被标记为分支;开启后同样纳入记忆观测。默认开。',
|
|
267
267
|
fTauHi: '唤起门槛 tauHi', fDeltaExp: '明确召回余量 deltaExp', fDeltaPro: '主动预取余量 deltaPro',
|
|
268
|
-
fTuningHint: '阈值由校准策略 JSON
|
|
268
|
+
fTuningHint: '阈值由校准策略 JSON 权威控制;此处为高级调参入口,修改保存后随下轮生效(预览版)。',
|
|
269
269
|
refineTitle: '唤起记录与语料精修', refineSub: '对每次唤起判断给出你的裁定(A 该激活/P 只预取/S 应抑制/H 有害/E 改目标),审批队列将用于离线重放与策略演进。',
|
|
270
270
|
refineEmpty: '(暂无唤起记录——需要 shadow 观测产生数据)', refineLoadErr: '加载失败: ',
|
|
271
271
|
sent1: '已入审批队列', semReady: '内置语义已就绪', semMissing: '语义包未下载(约130MB)', semStatusErr: '状态未知',
|
|
@@ -325,6 +325,33 @@ window.__ModuleLoader__.load({
|
|
|
325
325
|
hubStageDeprecated: '已弃用',
|
|
326
326
|
hubWhyTitle: '为什么还不能晋升:',
|
|
327
327
|
hubWhyCanPromote: '条件已满足,可以晋升。',
|
|
328
|
+
// ★M8-B 第6步(2026-09-23):双库视图(工作区库 / 通用库)。
|
|
329
|
+
hubScopeTitle: '技能库归属',
|
|
330
|
+
hubScopeGlobal: '通用库',
|
|
331
|
+
hubScopeWorkspace: '本工作区库',
|
|
332
|
+
hubScopeGlobalHint: '所有项目都能用(跨工作区共享)。',
|
|
333
|
+
hubScopeWorkspaceHint: '只有当前工作区能用。',
|
|
334
|
+
hubScopeDefault: '缺省落库:通用库',
|
|
335
|
+
hubScopeUnknownWs: '当前工作区未知 —— 暂不能收纳到工作区库(转移会明确报错,不会猜目录写入)。',
|
|
336
|
+
hubScopeCounts: function (g, w) {
|
|
337
|
+
var gs = (g < 0) ? '不可读' : (g + ' 条')
|
|
338
|
+
var ws = (w < 0) ? '不可归属' : (w + ' 条')
|
|
339
|
+
return '通用库 ' + gs + ' · 本工作区库 ' + ws
|
|
340
|
+
},
|
|
341
|
+
hubScopeCorrupt: '有库文件解析失败 —— 已拒写该库(避免静默清空),请先修复或备份。',
|
|
342
|
+
hubToGlobal: '提升为全局',
|
|
343
|
+
hubToWorkspace: '收纳到本工作区',
|
|
344
|
+
hubScopeBusy: '转移中…',
|
|
345
|
+
hubScopeMoved: '已转移:',
|
|
346
|
+
hubScopeNoop: '已在目标库(无需搬运)。',
|
|
347
|
+
hubScopeReasons: function (r) {
|
|
348
|
+
if (r === 'ws-unknown') return '当前工作区未知,无法写入工作区库。'
|
|
349
|
+
if (r === 'foreign-workspace') return '该条目属于别的工作区,不能直接搬到本工作区。'
|
|
350
|
+
if (r === 'not-found') return '两库中都没找到这条技能。'
|
|
351
|
+
if (r === 'phase-failed') return '两阶段提交失败,已回滚两库到转移前状态。'
|
|
352
|
+
if (r === 'scoped-io-unavailable') return '宿主未提供双库 IO,本次无法转移。'
|
|
353
|
+
return String(r || '未知原因')
|
|
354
|
+
},
|
|
328
355
|
hubWhyDiversity: function (d, need) { return '需要 ' + need + ' 个不同会话都用过它,目前只有 ' + d + ' 个 —— 换个场景再用一次它就能推进。' },
|
|
329
356
|
hubWhySuccess: function (n, need) { return '需要成功 ' + need + ' 次,目前 ' + n + ' 次 —— 再成功用一次它会自动变好。' },
|
|
330
357
|
hubWhyCorrectionRate: function (r, cap) { return '它的纠正率是 ' + r + ',超过了上限 ' + cap + ' —— 说明这套流程本身有问题,需要先改掉。' },
|
|
@@ -426,7 +453,7 @@ window.__ModuleLoader__.load({
|
|
|
426
453
|
fConsScheduleDays: '固化回看天数', fConsScheduleDaysHint: '定时固化读取最近 N 天日志;默认 7。',
|
|
427
454
|
fMaintSchedule: '定时 30 天蒸馏', fMaintScheduleHint: '每天到点自动把超过 30 天的旧日志蒸馏归档(等价 memory_maintain);无旧日志时零成本跳过。',
|
|
428
455
|
fMaintScheduleTime: '蒸馏触发时间', fMaintScheduleTimeHint: 'HH:MM,默认 10:00(与固化时间错开)。',
|
|
429
|
-
waterCardTitle: '上下文水位', waterAutoSrc: '自动检测', waterManualSrc: '手动设定', waterOfficialSrc: '官方路由容量', waterFallbackSrc: '回退默认值', waterNotMeasured: '本会话尚未测量(发送一轮后自动更新)', waterMeterOfficial: '官方计量', waterMeterHeuristic: '启发式降级', waterThresholdHint: '水位达到 {t} 时注入交接建议并自动补写账本;计量优先用官方 token-meter(与聊天框 context ring 同源),不可用时降级启发式估算;窗口优先取官方路由容量(request/context 的 contextWindow),其次按当前模型查 settings.yaml contextWindow,也可在下方手动覆盖;触发分母=官方声明窗口(provider 自报过硬限时取较小值),预留输出不参与扣减。',
|
|
456
|
+
waterCardTitle: '上下文水位', waterAutoSrc: '自动检测', waterManualSrc: '手动设定', waterOfficialSrc: '官方路由容量', waterFallbackSrc: '回退默认值', waterNotMeasured: '本会话尚未测量(发送一轮后自动更新)', waterWindowUnknown: '未测出上下文窗口大小', waterWindowUnknownHint: '未能从本会话读取模型窗口(旧会话记录可能已归档/回收)。发送一轮消息后可自动测出;或在下方手动填写「水位窗口」后即恢复。', waterMeterOfficial: '官方计量', waterMeterHeuristic: '启发式降级', waterThresholdHint: '水位达到 {t} 时注入交接建议并自动补写账本;计量优先用官方 token-meter(与聊天框 context ring 同源),不可用时降级启发式估算;窗口优先取官方路由容量(request/context 的 contextWindow),其次按当前模型查 settings.yaml contextWindow,也可在下方手动覆盖;触发分母=官方声明窗口(provider 自报过硬限时取较小值),预留输出不参与扣减。',
|
|
430
457
|
autoContTitle: '自动接续', autoContHint: '开启后:水位达到阈值即弹出确认卡——同意=立即接续,拒绝=本轮跳过(同一边界不再提示),30-40 秒无操作视为挂机自动接续;接续流程为先终止旧会话当前回合(未完成的回答会被打断)→ 让旧 Agent 刷新白板 PLAN 与交接账本并等它真正执行完 → 再建新会话(沿用工作区/模型/思考档位)并注入分层交接材料;触发后默认 30 分钟内不重复。', autoContOn: '自动接续:开', autoContOff: '自动接续:关', autoContThreshold: '阈值', autoContCountdown: '⏳ {s} 秒后自动接续…', autoContCancel: '取消', autoContConfirm: '本会话上下文已用 {p}%(按官方声明窗口计,官方约 80% 才压缩)。现在接续到新会话?点「同意接续」会先终止旧会话当前回合(未完成的回答会被打断),再让旧会话刷新白板与账本,然后建新会话。', autoContAgree: '同意接续', autoContReject: '拒绝', autoContTimeout: '{s} 秒后自动接续(视为挂机)', autoContRejected: '已跳过本次接续(下一个轮次边界再提醒)',
|
|
431
458
|
continueCardTitle: '一键接续', continueCardHint: '写好交接账本→创建新会话→把交接材料作为首条消息预载,并自动切换到新会话继续任务(接续材料含白板节选+最新账本;当前工作区无账本时自动取全局最近一篇)。', continueBtn: '一键接续到新会话', continueBusy: '处理中…',
|
|
432
459
|
fStyle: '反思风格', fStyleHint: '生活化 / 专业性 / 由内容决定。',
|
|
@@ -563,6 +590,33 @@ window.__ModuleLoader__.load({
|
|
|
563
590
|
hubStageDeprecated: 'deprecated',
|
|
564
591
|
hubWhyTitle: 'Why it cannot be promoted yet: ',
|
|
565
592
|
hubWhyCanPromote: 'All conditions met — ready to promote.',
|
|
593
|
+
// ★M8-B step 6 (2026-09-23): dual-library view (workspace / global).
|
|
594
|
+
hubScopeTitle: 'Skill library ownership',
|
|
595
|
+
hubScopeGlobal: 'Global library',
|
|
596
|
+
hubScopeWorkspace: 'This workspace',
|
|
597
|
+
hubScopeGlobalHint: 'Usable from every project (shared across workspaces).',
|
|
598
|
+
hubScopeWorkspaceHint: 'Only usable in the current workspace.',
|
|
599
|
+
hubScopeDefault: 'Default library: global',
|
|
600
|
+
hubScopeUnknownWs: 'Current workspace unknown — cannot move into the workspace library yet (a transfer fails explicitly instead of guessing a directory).',
|
|
601
|
+
hubScopeCounts: function (g, w) {
|
|
602
|
+
var gs = (g < 0) ? 'unreadable' : (g + '')
|
|
603
|
+
var ws = (w < 0) ? 'n/a' : (w + '')
|
|
604
|
+
return 'global ' + gs + ' · this workspace ' + ws
|
|
605
|
+
},
|
|
606
|
+
hubScopeCorrupt: 'A library file failed to parse — writes to it are refused (to avoid silently wiping it). Repair or back it up first.',
|
|
607
|
+
hubToGlobal: 'Promote to global',
|
|
608
|
+
hubToWorkspace: 'Move into workspace',
|
|
609
|
+
hubScopeBusy: 'Moving…',
|
|
610
|
+
hubScopeMoved: 'Moved: ',
|
|
611
|
+
hubScopeNoop: 'Already in the target library (nothing to move).',
|
|
612
|
+
hubScopeReasons: function (r) {
|
|
613
|
+
if (r === 'ws-unknown') return 'Current workspace is unknown, so the workspace library cannot be written.'
|
|
614
|
+
if (r === 'foreign-workspace') return 'This entry belongs to another workspace and cannot be moved into this one.'
|
|
615
|
+
if (r === 'not-found') return 'Not found in either library.'
|
|
616
|
+
if (r === 'phase-failed') return 'Two-phase commit failed; both libraries were rolled back to their pre-move state.'
|
|
617
|
+
if (r === 'scoped-io-unavailable') return 'Host provides no dual-library IO, so the move is unavailable.'
|
|
618
|
+
return String(r || 'unknown reason')
|
|
619
|
+
},
|
|
566
620
|
hubWhyDiversity: function (d, need) { return 'Needs ' + need + ' distinct sessions, currently ' + d + '.' },
|
|
567
621
|
hubWhySuccess: function (n, need) { return 'Needs ' + need + ' successes, currently ' + n + '.' },
|
|
568
622
|
hubWhyCorrectionRate: function (r, cap) { return 'Correction rate ' + r + ' exceeds cap ' + cap + '.' },
|
|
@@ -663,7 +717,7 @@ window.__ModuleLoader__.load({
|
|
|
663
717
|
fConsScheduleDays: 'Consolidation lookback (days)', fConsScheduleDaysHint: 'How many recent days of logs the scheduled consolidation reads; default 7.',
|
|
664
718
|
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.',
|
|
665
719
|
fMaintScheduleTime: 'Distill time', fMaintScheduleTimeHint: 'HH:MM, default 10:00 (staggered from the consolidation time).',
|
|
666
|
-
waterCardTitle: 'Context water level', waterAutoSrc: 'auto-detected', waterManualSrc: 'manual', waterOfficialSrc: 'route capacity', waterFallbackSrc: 'fallback default', waterNotMeasured: 'Not measured for this session yet (updates after the next turn)', waterMeterOfficial: 'official meter', waterMeterHeuristic: 'heuristic fallback', waterThresholdHint: 'Past {t} of the window, the handoff advisory is injected and the skeleton ledger is backfilled; metering prefers the official token-meter (same source as the chat context ring), falling back to the heuristic when unavailable; the window prefers the official route capacity (request/context contextWindow), then the active model in settings.yaml, or your manual override below; the trigger denominator is the official declared window (min with any provider-reported hard limit) — the reserved output budget is not deducted.',
|
|
720
|
+
waterCardTitle: 'Context water level', waterAutoSrc: 'auto-detected', waterManualSrc: 'manual', waterOfficialSrc: 'route capacity', waterFallbackSrc: 'fallback default', waterNotMeasured: 'Not measured for this session yet (updates after the next turn)', waterWindowUnknown: 'Context window size unknown', waterWindowUnknownHint: 'Could not read the model window from this session (its record may have been archived or recycled). Send a turn to detect it, or set the water-level window manually below.', 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; the trigger denominator is the official declared window (min with any provider-reported hard limit) — the reserved output budget is not deducted.',
|
|
667
721
|
autoContTitle: 'Auto continue', autoContHint: 'When on: a confirm card appears once the water level passes the threshold — Agree = continue now, Decline = skip this boundary (no re-prompt for it), no answer for 30-40s = unattended, continue automatically. The handoff sequence is: stop the old turn first (an unfinished answer will be interrupted) → have the old agent refresh PLAN + ledger and wait until it has really run → then create the new session, which 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: 'Context at {p}% of the official declared window (official compaction starts ~80%). Continue into a new session now? Agreeing stops the old turn first (an unfinished answer will be interrupted), then the old agent refreshes PLAN + ledger before the new session is created.', autoContAgree: 'Continue', autoContReject: 'Decline', autoContTimeout: 'auto-continue in {s}s (unattended)', autoContRejected: 'Skipped this handoff (will ask again at the next turn boundary)',
|
|
668
722
|
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…',
|
|
669
723
|
fStyle: 'Reflection style', fStyleHint: 'Life-style / Professional / Auto.',
|
|
@@ -771,11 +825,11 @@ window.__ModuleLoader__.load({
|
|
|
771
825
|
// (设置页「提示词层级」展示的就是这份镜像,漂移会让用户看到的与实际注入不符)。
|
|
772
826
|
snapshotInscription: '[铭文 · 每轮提醒 {date}]\n'
|
|
773
827
|
+ '【收尾自检】本轮有实质产出才做;纯只读/闲聊轮跳过。三个方向 + 分类别丢:\n'
|
|
774
|
-
+ '① 写记忆文件 memory_log —— kind 按性质选,**不要一律 fact**:rule
|
|
775
|
-
+ '② 更新白板与账本 —— ★**硬映射(满足即必须做,别自行判断"算不算"变化)**:\n · 本轮**改过 lib/ 下任何文件**(含测试/工具) ⇒ 必写 **memory_note(kind=handoff)** 四段式账本;\n · 白板"当前进度"与本轮结束时的**事实不符**(回归数字/已完成项/下一步) ⇒ 必做 **memory_note(kind=plan)** 重写白板;\n · 仅补充一条可复用结论 ⇒ 才用 **memory_note(kind=note, action=append)**。\n ⚠️ **kind=note 不等于白板/账本**——只写 note
|
|
776
|
-
+ '③ 长期记忆判断(三个去处)—— 跨会话有用的 → memory_note / 跨项目规则 → memory_user / **规则条目增删改(用户级硬约束,含过时条目真删)→ memory_rules** / **跑通且可复用的多步流程 → memory_procedure
|
|
777
|
-
+ '④ 结论失效/被取代 —— 传 `supersedes=` 旧条目 mem_id 标 **superseded(被更新结论取代)**;**当时就做错了要撤回**则传 `retract=` 标 **retracted
|
|
778
|
-
+ '⑤ 看板落列 —— 白板/账本内容要进面板看板泳道,须在标题或正文写 tag:`type:goal` / `type:state` / `type:dead-end` / `type:progress
|
|
828
|
+
+ '① 写记忆文件 memory_log —— kind 按性质选,**不要一律 fact**:rule=用户约束/约定、preference=偏好、fact=事实记录、todo=待办;\n'
|
|
829
|
+
+ '② 更新白板与账本 —— ★**硬映射(满足即必须做,别自行判断"算不算"变化)**:\n · 本轮**改过 lib/ 下任何文件**(含测试/工具) ⇒ 必写 **memory_note(kind=handoff)** 四段式账本;\n · 白板"当前进度"与本轮结束时的**事实不符**(回归数字/已完成项/下一步) ⇒ 必做 **memory_note(kind=plan)** 重写白板;\n · 仅补充一条可复用结论 ⇒ 才用 **memory_note(kind=note, action=append)**。\n ⚠️ **kind=note 不等于白板/账本**——只写 note 不得宣称"已更新白板与账本"(失职);write 直接写 docs/ 的 md 不算插件记忆。\n'
|
|
830
|
+
+ '③ 长期记忆判断(三个去处)—— 跨会话有用的 → memory_note / 跨项目规则 → memory_user / **规则条目增删改(用户级硬约束,含过时条目真删)→ memory_rules** / **跑通且可复用的多步流程 → memory_procedure**(技能库入口;建议填 successCriteria;**写前先用 `memory_procedure_list` 查重**:只读,能看它属哪个库、为何未晋升);\n'
|
|
831
|
+
+ '④ 结论失效/被取代 —— 传 `supersedes=` 旧条目 mem_id 标 **superseded(被更新结论取代)**;**当时就做错了要撤回**则传 `retract=` 标 **retracted(撤回)**并附 `retractReason=` 说明错在哪(前者有后继;后者即教训);标错了用 `restore=` 撤回;\n'
|
|
832
|
+
+ '⑤ 看板落列 —— 白板/账本内容要进面板看板泳道,须在标题或正文写 tag:`type:goal` / `type:state` / `type:dead-end` / `type:progress`(含「版本归档」泳道);不写则系统按标题猜,常落空;\n'
|
|
779
833
|
+ '⑥ 语体 —— 客观陈述、第三人称,只留可复用的事实/决策/规则/路径(不写"我考虑/我排查/我想")。',
|
|
780
834
|
snapshotTail: '</memory_system>',
|
|
781
835
|
}
|
|
@@ -806,6 +860,32 @@ window.__ModuleLoader__.load({
|
|
|
806
860
|
|
|
807
861
|
// ───────────────────────── 更新弹窗 / 首次指导 ─────────────────────────
|
|
808
862
|
var CHANGELOG = {
|
|
863
|
+
'3.1.7': { zh: [
|
|
864
|
+
'★ 修复:一键接续有时会把「最近活跃会话」认成一个后台子代理,导致新会话建到错误的工作区,而且不报错。现在会正确识别源会话;真的定位不到时会明确告诉你,不再静默。',
|
|
865
|
+
'★ 修复:写白板时锚点标记的空格被吃掉,导致卡片识别失败。现在锚点行会被原样保留。',
|
|
866
|
+
'★ 修复:面板里不显示水位。以前读不到窗口大小时整张卡会直接消失(连「尚未测量」提示也被一起藏住);现在会照常显示,并说明原因与恢复方法。',
|
|
867
|
+
'本版三处修复都在宿主加载期代码里,需重启 dsh web 后生效。',
|
|
868
|
+
], en: [
|
|
869
|
+
'★ Fix: the continue handoff could mistake a background subagent for your most recent session and create the new session in the wrong workspace — silently. It now resolves the correct source session, and tells you when it genuinely cannot.',
|
|
870
|
+
'★ Fix: the anchor marker lost its space when writing the board, breaking card recognition. Anchor lines are now preserved verbatim.',
|
|
871
|
+
'★ Fix: the context water level was not shown. When the window size was unknown the whole card disappeared (hiding the "not measured yet" note with it); it now renders and explains the cause and the fix.',
|
|
872
|
+
'All three fixes live in host-side code — restart dsh web for them to take effect.',
|
|
873
|
+
] },
|
|
874
|
+
'3.1.6': { zh: [
|
|
875
|
+
'★★ 紧急修复:配置迁移自 3.1.0 起从未真正执行——宿主每次加载都因内部变量作用域错误抛异常,而异常被兜底逻辑静默吞掉。结果是"你保存过的设置会变回默认值"(注入预算/记忆根目录/看板模式/容量上限等),且日志里没有任何报错,极易被误判成"自己没配"。',
|
|
876
|
+
'现在配置改名与补齐已改到唯一的加载汇聚点,并强制走损坏隔离读。升级后你此前的设置会从旧配置文件自动补回来,无需手动重配。',
|
|
877
|
+
'重要:本版修的是宿主加载期代码,**必须重启 dsh web 才生效**。',
|
|
878
|
+
'同时修复:记忆中枢的数据目录(hub)曾因一个预先存在的空壳目录被判为"已存在"而跳过迁移,导致 56 条技能与 9.3 MB 事实在重启后不可见;现改为逐文件语义补齐,绝不覆盖真实数据。',
|
|
879
|
+
'内部重构:全仓"删净 pre"——源码名即发布名,发布构建退化为纯复制;配置文件名与前端本地存储键一并改名,均带旧名兼容读与首次迁移,你的主题/字体/面板几何不丢。',
|
|
880
|
+
'构建线修复三处(回归测不到、只有真跑构建才暴露):过渡垫片会被覆盖到真实模块上(整包报废)、上游回流清单写旧名导致闸门拒构建、残留闸门引用了已删除的变量导致构建崩溃。',
|
|
881
|
+
], en: [
|
|
882
|
+
'★★ Emergency fix: config migration has never actually run since 3.1.0 — the host threw an internal scope error on every load and the fail-soft handler silently swallowed it. Result: "settings you saved silently revert to defaults" (inject budget, memory root, board mode, capacity caps), with no error in the logs, easily misread as "I never configured it".',
|
|
883
|
+
'Migration now lives at the single load choke point and goes through the corruption-quarantine reader. On upgrade, your previous settings are merged back from the legacy config file automatically.',
|
|
884
|
+
'Important: this fixes host load-time code, so **dsh web must be restarted** to take effect.',
|
|
885
|
+
'Also fixed: the memory-hub data directory was skipped because a pre-existing empty shell dir looked like a valid target, hiding 56 skills and 9.3 MB of facts after restart; now files are filled in semantically and real data is never overwritten.',
|
|
886
|
+
'Internal refactor: full "de-pre" — source names are now the published names and the release build is a plain copy; config filename and frontend localStorage keys were renamed with legacy fallback reads and first-read migration, so your theme/fonts/panel geometry are preserved.',
|
|
887
|
+
'Three build-line fixes that the test suite cannot catch (only a real build exposes them): shim files overwriting real modules (would ship a broken package), an upstream-reconcile manifest still listing old names (gate refused to build), and a residual gate referencing a deleted variable (build crashed).',
|
|
888
|
+
] },
|
|
809
889
|
'3.1.5': { zh: [
|
|
810
890
|
'★ 修复:记忆正文里出现半角双花括号(例如记下「Vue 模板用 {{ count }} 做插值」),会让该会话之后**每一次模型请求全部失败**。现在渲染尾注前会先把它们换成同形全角字符,不再触发宿主插值抛错。',
|
|
811
891
|
'说明:代价是这类括号在注入文本里会显示成全角形({{ count }}),内容不丢、含义不变。',
|
|
@@ -1351,7 +1431,26 @@ window.__ModuleLoader__.load({
|
|
|
1351
1431
|
|
|
1352
1432
|
// ───────────────────────── API ─────────────────────────
|
|
1353
1433
|
// 路由前缀唯一常量(2026-09-10 第 0 步止血):表中每条路径由它拼出,不再逐个重复全串。
|
|
1354
|
-
|
|
1434
|
+
// ★去 pre(2026-09-23):localStorage 键 dsh-auto-memory-pre.* → dsh-auto-memory.*。
|
|
1435
|
+
// 一次性迁移:首次载入把旧前缀键搬到新前缀(字体/主题/面板几何/引导进度等不丢)。
|
|
1436
|
+
// 幂等(搬完删旧键)+ fail-soft(任何异常都不得阻断前端启动)。
|
|
1437
|
+
;(function migrateLegacyLsKeys() {
|
|
1438
|
+
try {
|
|
1439
|
+
var OLD_LS = 'dsh-auto-memory-pre.', NEW_LS = 'dsh-auto-memory.'
|
|
1440
|
+
var doomed = []
|
|
1441
|
+
for (var li = 0; li < localStorage.length; li++) {
|
|
1442
|
+
var lk = localStorage.key(li)
|
|
1443
|
+
if (lk && lk.indexOf(OLD_LS) === 0) doomed.push(lk)
|
|
1444
|
+
}
|
|
1445
|
+
for (var lj = 0; lj < doomed.length; lj++) {
|
|
1446
|
+
var nk = NEW_LS + doomed[lj].slice(OLD_LS.length)
|
|
1447
|
+
if (localStorage.getItem(nk) === null) localStorage.setItem(nk, localStorage.getItem(doomed[lj]))
|
|
1448
|
+
localStorage.removeItem(doomed[lj])
|
|
1449
|
+
}
|
|
1450
|
+
if (doomed.length) console.info('[dsh-auto-memory] migrated ' + doomed.length + ' legacy localStorage keys')
|
|
1451
|
+
} catch (e) { /* fail-soft */ }
|
|
1452
|
+
})();
|
|
1453
|
+
var ROUTE_PREFIX = '/api/dsh-auto-memory'
|
|
1355
1454
|
// 宿主半边(lib/index.js 的 API)声明的是同一批路径 —— 两个半边是独立 bundle(本文件经 __ModuleLoader__ 手写加载,
|
|
1356
1455
|
// 无法 import 宿主模块),所以"只有一份"这件事由 tests/smoke/ 下的路径表一致性锁保证:
|
|
1357
1456
|
// 客户端路径 ⊆ 宿主路径、宿主独有路径只允许显式白名单、本文件表外不得再出现裸路径字面量。
|
|
@@ -3732,12 +3831,70 @@ window.__ModuleLoader__.load({
|
|
|
3732
3831
|
})
|
|
3733
3832
|
.catch(function (e) { setActMsg(action + ' failed: ' + String(e && e.message)) })
|
|
3734
3833
|
}
|
|
3834
|
+
// ★M8-B 第6步(2026-09-23):归属转移(提升为全局 / 收纳到工作区)。
|
|
3835
|
+
// 与 hubAct 分开:动作名与参数不同(scope 而非 v),且必须把 `migrate()` 的逐条结果
|
|
3836
|
+
// (含 rolledBack / reason)显示出来 —— 两阶段提交的失败是要让人看见的,
|
|
3837
|
+
// 静默成 "done" 等于把回滚藏起来,正是用户列为「功能坏了」的那种表现。
|
|
3838
|
+
function hubScopeTransfer(procedureId, toScope) {
|
|
3839
|
+
setActMsg(t('hubScopeBusy'))
|
|
3840
|
+
fetch(API.memoryHub, { method: 'POST',
|
|
3841
|
+
headers: { 'Content-Type': 'application/json' },
|
|
3842
|
+
body: JSON.stringify({ action: 'transfer-scope', procedureId: procedureId, scope: toScope }) })
|
|
3843
|
+
.then(function (r) { return r.json() })
|
|
3844
|
+
.then(function (j) {
|
|
3845
|
+
var one = (j && Array.isArray(j.results) && j.results[0]) || {}
|
|
3846
|
+
if (j && j.ok && one.ok) {
|
|
3847
|
+
setActMsg(t('hubScopeMoved') + (toScope === 'workspace' ? t('hubScopeWorkspace') : t('hubScopeGlobal')) + (one.noop ? '(' + t('hubScopeNoop') + ')' : ''))
|
|
3848
|
+
} else {
|
|
3849
|
+
// 失败必须带原因 + 是否已回滚;两者都不许省略。
|
|
3850
|
+
setActMsg(t('hubScopeReasons')(one.reason || (j && j.reason) || (j && j.error))
|
|
3851
|
+
+ (j && j.rolledBack ? ' ⚠ ' + t('hubScopeReasons')('phase-failed') : ''))
|
|
3852
|
+
}
|
|
3853
|
+
setRefresh(function (x) { return x + 1 })
|
|
3854
|
+
})
|
|
3855
|
+
.catch(function (e) { setActMsg(t('hubScopeReasons')(String(e && e.message))) })
|
|
3856
|
+
}
|
|
3735
3857
|
if (err) return h('div', { 'data-dam-hint': '' }, t('searchFailed') + err)
|
|
3736
3858
|
if (!data) return h(Loading, { label: t('loading') })
|
|
3737
3859
|
var rows = []
|
|
3738
3860
|
var procs = data.procedures
|
|
3739
3861
|
var facts = data.facts
|
|
3740
3862
|
var epis = data.episodic
|
|
3863
|
+
// ★M8-B 第6步(2026-09-23):**双库视图** —— 回答用户三问:
|
|
3864
|
+
// ① 这条技能属于哪个库?(归属徽标,逐条)
|
|
3865
|
+
// ② 当前是哪个库、各库几条、工作区库现在能不能写?(本区块头部)
|
|
3866
|
+
// ③ 怎么改判归属?(提升为全局 / 收纳到本工作区,逐条按钮)
|
|
3867
|
+
// 数据源 = 宿主 /memory-hub 的 `scopeView`(只读、无真实路径)+ 每行的 scope 字段。
|
|
3868
|
+
// 工作区未知时按钮仍显示但**点击会明确报错**(宿主 migrate 返回 ws-unknown),
|
|
3869
|
+
// 比灰掉按钮更能说明「为什么不行」—— 与 hubScopeUnknownWs 文案配套。
|
|
3870
|
+
var sv = data.scopeView
|
|
3871
|
+
if (sv) {
|
|
3872
|
+
var svHint = (sv.workspaceReady ? t('hubScopeWorkspaceHint') : t('hubScopeUnknownWs'))
|
|
3873
|
+
+ ' ' + (sv.globalDirCorrupt || sv.workspaceDirCorrupt ? '⚠ ' + t('hubScopeCorrupt') : '')
|
|
3874
|
+
rows.push(h(Card, { title: t('hubScopeTitle') },
|
|
3875
|
+
h('div', { 'data-dam-content': '' },
|
|
3876
|
+
h('div', null, h('b', null, t('hubScopeDefault')), h('span', { 'data-dam-hint': '', style: { marginLeft: '6px' } }, t('hubScopeCounts')(sv.globalCount, sv.workspaceCount))),
|
|
3877
|
+
h('div', { 'data-dam-hint': '', style: { marginTop: '3px', opacity: .8 } }, svHint))))
|
|
3878
|
+
}
|
|
3879
|
+
// 归属徽标:通用库 / 本工作区库。缺省 global 由宿主投影补齐,这里不再兜判。
|
|
3880
|
+
function scopeBadge(p) {
|
|
3881
|
+
var isWs = p && p.scope === 'workspace'
|
|
3882
|
+
return h('span', {
|
|
3883
|
+
'data-dam-scope': isWs ? 'workspace' : 'global',
|
|
3884
|
+
'data-dam-hint': '',
|
|
3885
|
+
title: isWs ? t('hubScopeWorkspaceHint') : t('hubScopeGlobalHint'),
|
|
3886
|
+
style: { marginLeft: '6px', opacity: .85 },
|
|
3887
|
+
}, '[' + (isWs ? t('hubScopeWorkspace') : t('hubScopeGlobal')) + (p && p.addedBy ? ' · ' + p.addedBy : '') + ']')
|
|
3888
|
+
}
|
|
3889
|
+
// 转移按钮:目标库 = 与当前归属相反的那个。已在目标库时宿主会回 noop(幂等)。
|
|
3890
|
+
function scopeButtons(p) {
|
|
3891
|
+
var isWs = p && p.scope === 'workspace'
|
|
3892
|
+
return h('button', {
|
|
3893
|
+
'data-dam-btn': '', 'data-dam-scope-to': isWs ? 'global' : 'workspace',
|
|
3894
|
+
style: { fontSize: 'calc(10.5px * var(--dam-scale))', padding: '3px 10px' },
|
|
3895
|
+
onClick: function () { hubScopeTransfer(p.procedureId, isWs ? 'global' : 'workspace') },
|
|
3896
|
+
}, isWs ? t('hubToGlobal') : t('hubToWorkspace'))
|
|
3897
|
+
}
|
|
3741
3898
|
// 技能层(最精彩: 反复成功的流程固化为 skill)
|
|
3742
3899
|
rows.push(h('div', { 'data-dam-hint': '', style: { marginTop: '8px' } }, t('hubSkills')))
|
|
3743
3900
|
var activeList = (procs && procs.active) || []
|
|
@@ -3749,6 +3906,8 @@ window.__ModuleLoader__.load({
|
|
|
3749
3906
|
var risk = p.riskLevel === 'high' ? (locale === 'zh' ? '· 高风险需确认' : '· high-risk') : ''
|
|
3750
3907
|
return h('div', { 'data-dam-content': '', key: p.procedureId, style: { padding: '4px 0', borderBottom: '1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(255,255,255,.16)) 40%, transparent)' } },
|
|
3751
3908
|
h('div', null, h('b', null, p.title), h('span', { 'data-dam-hint': '', style: { marginLeft: '6px' } }, (locale === 'zh' ? '成功 ' : 'success ') + (p.evidence ? p.evidence.success : 0) + ' · ' + (locale === 'zh' ? '会话 ' : 'sessions ') + (p.evidence ? p.evidence.sessions : 0) + ' ' + risk),
|
|
3909
|
+
// ★M8-B 第6步:已激活技能也要显示归属库(用户要「在当前库时,显示激活了哪些 skill」)
|
|
3910
|
+
scopeBadge(p),
|
|
3752
3911
|
h('button', { 'data-dam-btn': '', style: { marginLeft: '8px', fontSize: 'calc(10px * var(--dam-scale))', padding: '2px 8px' }, onClick: function () { hubAct('deprecate', p.procedureId) } }, locale === 'zh' ? '弃用' : 'deprecate'),
|
|
3753
3912
|
h('button', { 'data-dam-btn': '', style: { marginLeft: '4px', fontSize: 'calc(10px * var(--dam-scale))', padding: '2px 8px' }, onClick: function () { hubAct('pin', p.procedureId, !(procs && procs.pipeline && procs.pipeline.concat(activeList).find(function (x) { return x.procedureId === p.procedureId && x.pinned }))) } }, locale === 'zh' ? '置顶' : 'pin')))
|
|
3754
3913
|
})))
|
|
@@ -3765,6 +3924,8 @@ window.__ModuleLoader__.load({
|
|
|
3765
3924
|
'[' + stageLabel(p.stage) + ']'
|
|
3766
3925
|
+ (p.observationOnly ? (locale === 'zh' ? ' · 观察(不参与自动晋升)' : ' · observation-only') : '')
|
|
3767
3926
|
+ (p.pinned ? ' 📌' : '') + (p.riskLevel === 'high' ? ' ⚠' : '')),
|
|
3927
|
+
// ★M8-B 第6步:归属库徽标(这条技能住在哪个库)
|
|
3928
|
+
scopeBadge(p),
|
|
3768
3929
|
// ★R5:evidence 给人话(不是裸数字)
|
|
3769
3930
|
h('span', { 'data-dam-hint': '', style: { marginLeft: '6px' } }, t('hubEvLine')(ev))),
|
|
3770
3931
|
// ★R2:**为什么不能晋升** —— 必须在界面上说清楚
|
|
@@ -3786,12 +3947,15 @@ window.__ModuleLoader__.load({
|
|
|
3786
3947
|
: null))
|
|
3787
3948
|
: h('div', { 'data-dam-hint': '', style: { marginTop: '4px', opacity: .7 } }, t('hubNoSteps')),
|
|
3788
3949
|
h('div', { style: { display: 'flex', gap: '6px', marginTop: '5px' } },
|
|
3950
|
+
// ★M8-B 第6步:归属转移(提升为全局 / 收纳到本工作区)——与晋升是同级的**库归属**操作,
|
|
3951
|
+
// 不是晋升门限,因此不受 p.promotion/observationOnly 影响(只读线索也能改判归属)。
|
|
3952
|
+
scopeButtons(p),
|
|
3789
3953
|
// issue #30:纯 episode 观察行**结构上**不可能晋升(无 successCriteria),
|
|
3790
3954
|
// 继续显示"晋升"按钮只会让人反复点击却看不到变化 ⇒ 直接隐藏并打观察标。
|
|
3791
3955
|
// ★ 2026-09-21 bugfix(A-9):原条件只挡 observationOnly,与 store 真实门限**不一致** ——
|
|
3792
3956
|
// 缺 successCriteria / 有 correction / correctionRate 超限的条目照样显示按钮,
|
|
3793
3957
|
// 用户点下去只得到一行 keep 灰字,感受即「批准不了」。
|
|
3794
|
-
// 现改为**直接依赖只读投影** `p.promotion`(宿主 memory-hub 用 evaluatePromotion 生成,
|
|
3958
|
+
// 现改为**直接依赖只读投影** `p.promotion`(宿主 memory-hub-pre 用 evaluatePromotion 生成,
|
|
3795
3959
|
// 纯只读、不写盘):decision==='promote' 才给按钮,否则界面已由 R2 区块说明原因。
|
|
3796
3960
|
// fail-soft:投影缺失(旧宿主/异常)时退回原 observationOnly 判据,不做过度拦截。
|
|
3797
3961
|
(!p.observationOnly && (!p.promotion || p.promotion.decision === 'promote'))
|
|
@@ -3803,7 +3967,7 @@ window.__ModuleLoader__.load({
|
|
|
3803
3967
|
(p.promotion && p.promotion.decision === 'ask')
|
|
3804
3968
|
&& h('button', { 'data-dam-btn': '', style: { fontSize: 'calc(10.5px * var(--dam-scale))', padding: '3px 10px', fontWeight: 650 }, onClick: function () { hubAct('approve', p.procedureId) } }, locale === 'zh' ? '批准(人工)' : 'approve (manual)'),
|
|
3805
3969
|
// ★B/C(2026-09-22):人工**强制晋升** —— 只在「拦它的门**全是统计门**」时给出。
|
|
3806
|
-
// 判据来自宿主只读投影 p.promotion.overridable(memory-hub 用 promotionOverrideView 生成,
|
|
3970
|
+
// 判据来自宿主只读投影 p.promotion.overridable(memory-hub-pre 用 promotionOverrideView 生成,
|
|
3807
3971
|
// 只读、零副作用、且**不重复实现门限** ⇒ 结构上不可能与 promote() 漂移)。
|
|
3808
3972
|
// 结构门拦(已弃用 / 观察型 / 无 successCriteria / 有纠正记录)时**不给按钮** ——
|
|
3809
3973
|
// 否则就是再造一个「点了没反应」的假通道(A-9 收掉旧按钮的正是这个原因)。
|
|
@@ -4261,6 +4425,14 @@ window.__ModuleLoader__.load({
|
|
|
4261
4425
|
var fromSidForCarry = String(lastRefreshSessionId || currentSessionIdClient() || '')
|
|
4262
4426
|
var d = await apiPost(API.handoffContinue, fromSidForCarry ? { fromSessionId: fromSidForCarry } : {})
|
|
4263
4427
|
if (!d || !d.ok) throw new Error((d && d.error) || 'no handoff material')
|
|
4428
|
+
// ★2026-09-23:源会话工作区无法定位时,宿主会回退到「当前工作区」并把 wsFallback 置真。
|
|
4429
|
+
// 这正是「接续落到错误工作区」的现场(旧会话文件被 GC 搬走时)。不再静默:明确告知用户
|
|
4430
|
+
// 新会话建在哪个工作区、以及为什么 —— 用户据此可判断要不要手动切回正确工作区。
|
|
4431
|
+
if (d.wsFallback) {
|
|
4432
|
+
onMsg(locale === 'zh'
|
|
4433
|
+
? '⚠ 无法定位源会话的工作区(旧会话记录可能已被子代理回收搬走),新会话将建在当前工作区:' + (d.ws || '(未知)')
|
|
4434
|
+
: '⚠ Could not locate the source session\'s workspace (its record may have been GC\'d). The new session will use the current workspace: ' + (d.ws || '(unknown)'))
|
|
4435
|
+
}
|
|
4264
4436
|
var newId = await executeContinue(d, onMsg)
|
|
4265
4437
|
try { localStorage.setItem('dsh-auto-memory.autoCont.last', String(Date.now())) } catch (eL) {}
|
|
4266
4438
|
return { ok: true, sessionId: newId, workspaceId: d.workspaceId || '', model: d.model || '', reasoningEffort: d.reasoningEffort || '' }
|
|
@@ -4474,22 +4646,28 @@ window.__ModuleLoader__.load({
|
|
|
4474
4646
|
})
|
|
4475
4647
|
if (secKids.length) rows.push(h(Card, { title: locale === 'zh' ? '新版看板 · 段视图' : 'Graph board · sections' }, h('div', { 'data-dam-content': '' }, secKids)))
|
|
4476
4648
|
}
|
|
4477
|
-
|
|
4649
|
+
// ★2026-09-23 修复「目录里不显示水位」:旧条件 `wl.window > 0` 使**未测出窗口时整张卡消失**,
|
|
4650
|
+
// 连本节下方那句「本会话尚未测量」提示都不可达(它原本就在同一门内)⇒ 用户只看到"没有水位"
|
|
4651
|
+
// 而得不到任何解释。现改为:只要有 waterLevel 对象就渲染,未测出窗口时如实说明原因与下一步。
|
|
4652
|
+
if (wl && (wl.window > 0 || wl.at || Object.keys(wl).length)) {
|
|
4478
4653
|
// 2.2.4:百分比显示真实水位(旧版截断到 150% 会把「窗口算错」伪装成「刚好超一点」);进度条宽度仍按 100% 截断。
|
|
4479
4654
|
var rawPct = Math.round((wl.ratio || 0) * 100)
|
|
4480
4655
|
var pct = Math.min(rawPct, 999)
|
|
4656
|
+
var hasWin = (wl.window || 0) > 0
|
|
4481
4657
|
var barColor = rawPct >= 80 ? '#d4744f' : rawPct >= 50 ? '#d4a94f' : 'var(--dam-accent, #4f7cff)'
|
|
4482
4658
|
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')))
|
|
4483
4659
|
rows.unshift(h(Card, { title: t('waterCardTitle') }, h('div', { 'data-dam-content': '' }, [
|
|
4484
4660
|
h('div', { style: { fontSize: 'calc(12px * var(--dam-scale))', marginBottom: '6px' } },
|
|
4485
|
-
(wl.tokens || 0).toLocaleString() + ' / ' + wl.window.toLocaleString() + ' token · ' + pct + '%',
|
|
4661
|
+
hasWin ? ((wl.tokens || 0).toLocaleString() + ' / ' + wl.window.toLocaleString() + ' token · ' + pct + '%') : t('waterWindowUnknown'),
|
|
4486
4662
|
' ', h('span', { 'data-dam-hint': '' }, srcLabel),
|
|
4487
4663
|
' ', h('span', { 'data-dam-hint': '' }, wl.meter ? (wl.meter.indexOf('official') === 0 ? '· ' + t('waterMeterOfficial') + '(' + String(wl.meter).slice(9) + ')' : '· ' + t('waterMeterHeuristic')) : '')),
|
|
4488
4664
|
// 本会话尚未测量(刚重启/刚切过来还没发消息):明说,而不是显示成 0 让人以为上下文是空的
|
|
4489
4665
|
wl.at ? null : h('div', { 'data-dam-hint': '', style: { marginBottom: '4px' } }, t('waterNotMeasured')),
|
|
4490
|
-
h('div', { style: { height: '6px', borderRadius: '3px', background: 'color-mix(in srgb, currentColor 12%, transparent)', overflow: 'hidden' } },
|
|
4491
|
-
h('div', { style: { height: '100%', width: Math.min(pct, 100) + '%', background: barColor, borderRadius: '3px' } })),
|
|
4492
|
-
h('div', { 'data-dam-hint': '', style: { marginTop: '4px' } },
|
|
4666
|
+
hasWin ? h('div', { style: { height: '6px', borderRadius: '3px', background: 'color-mix(in srgb, currentColor 12%, transparent)', overflow: 'hidden' } },
|
|
4667
|
+
h('div', { style: { height: '100%', width: Math.min(pct, 100) + '%', background: barColor, borderRadius: '3px' } })) : null,
|
|
4668
|
+
h('div', { 'data-dam-hint': '', style: { marginTop: '4px' } }, hasWin
|
|
4669
|
+
? t('waterThresholdHint').replace('{t}', String(wl.threshold || 0.75))
|
|
4670
|
+
: t('waterWindowUnknownHint')),
|
|
4493
4671
|
])))
|
|
4494
4672
|
}
|
|
4495
4673
|
if (autoCfg && wl.window > 0) {
|
|
@@ -4842,11 +5020,19 @@ function StatsTab() {
|
|
|
4842
5020
|
|
|
4843
5021
|
useEffect(function () {
|
|
4844
5022
|
var alive = true
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
5023
|
+
function load() {
|
|
5024
|
+
fetch(API.recallStats)
|
|
5025
|
+
.then(function (res) { return res.json() })
|
|
5026
|
+
.then(function (j) { if (alive) { setErr(''); setData(j) } })
|
|
5027
|
+
.catch(function (ex) { if (alive) setErr(String((ex && ex.message) || ex)) })
|
|
5028
|
+
}
|
|
5029
|
+
load()
|
|
5030
|
+
// ★A-11b(2026-09-23):面板开着时**自己刷新** —— 此前只在挂载/点「复位」时拉一次,
|
|
5031
|
+
// 用户盯着统计页数字也永远不动(实测「召回统计不更新」的第二个真因,另一个是
|
|
5032
|
+
// 宿主侧从不落盘,见 recall-stats.js 的 maybeSave)。
|
|
5033
|
+
// 15s 轮询是纯 GET 只读查询;卸载时清掉,不留悬挂定时器。
|
|
5034
|
+
var iv = setInterval(load, 15000)
|
|
5035
|
+
return function () { alive = false; clearInterval(iv) }
|
|
4850
5036
|
}, [tick])
|
|
4851
5037
|
|
|
4852
5038
|
function reset() {
|
|
@@ -5990,11 +6176,8 @@ function WorkspaceTab() {
|
|
|
5990
6176
|
// 恒供给布尔 procedureInjectEnabled 而**结构性不可达** ⇒ 该开关此前是摆设(会翻面、写盘成功、宿主读不到)。
|
|
5991
6177
|
{ key: 'procedureInjectEnabled', name: locale === 'zh' ? '技能固化与注入' : 'Skill crystallization', sub: locale === 'zh' ? '重复流程固化为 checklist 自动附上(本开关只管「附不附」;晋升审批在「唤起回顾」页,与本开关无关)' : 'Turn repeated flows into auto-attached checklists (this switch only controls attaching; promotion review lives in the Recall review tab and is unrelated)', where: locale === 'zh' ? '记忆中枢' : 'Memory Hub' },
|
|
5992
6178
|
// ★T10(2026-09-20):机械 procedure 切片开关。
|
|
5993
|
-
//
|
|
5994
|
-
//
|
|
5995
|
-
// 默认关:procedural 线只留模型直写(memory_procedure)与人工两条正经通路。
|
|
5996
|
-
// ⚠️ 解耦:本项**只**影响 procedure 切片,fact 分支/episode 巩固/judgement 消费不动。
|
|
5997
|
-
{ key: 'hubMechanicalProcedureFeedEnabled', name: locale === 'zh' ? '机械流程切片(不推荐)' : 'Mechanical flow slicing (not recommended)', sub: locale === 'zh' ? '把情节意图截断成"技能",无模型介入、可读性差;关=只保留模型自己写的技能(推荐关)' : 'Truncates episode intent into "skills" with no model involvement; off = only model-authored skills (recommended)', where: locale === 'zh' ? '记忆中枢' : 'Memory Hub' },
|
|
6179
|
+
// ★A-1(2026-09-23):机械切片已整条删除(用户裁定弃用)⇒ 向导清单同步去掉该项,
|
|
6180
|
+
// 避免向导承诺一个已不存在的功能(点开找不到控件)。
|
|
5998
6181
|
] },
|
|
5999
6182
|
{ art: 'rocket', core: '', kicker: locale === 'zh' ? '完成' : 'READY', final: true,
|
|
6000
6183
|
title: locale === 'zh' ? '一切就绪' : 'All set',
|
|
@@ -6459,9 +6642,9 @@ function WorkspaceTab() {
|
|
|
6459
6642
|
return null
|
|
6460
6643
|
}
|
|
6461
6644
|
// ★P10-T1/T2(2026-09-22):13 个注入分区的**显示文案**。成员与顺序以宿主下发的
|
|
6462
|
-
// `promptSections` 为准(宿主常量
|
|
6645
|
+
// `promptSections` 为准(宿主常量 PROMPT_SECTION_KEYS_PRE_V1 是唯一真源),本表只提供文案;
|
|
6463
6646
|
// 键集合必须与宿主常量相等 —— 由守卫 smoke-test-t1-prompt-sections-pre.mjs 断言。
|
|
6464
|
-
// offZh/offEn 只给 must 段(宿主
|
|
6647
|
+
// offZh/offEn 只给 must 段(宿主 PROMPT_SECTION_MUST_PRE_V1),其余用通用提示。
|
|
6465
6648
|
var PROMPT_SECTION_TEXT = {
|
|
6466
6649
|
'rules-section': { zh: '规则段(用户级硬约束)', en: 'Rules section (hard constraints)' },
|
|
6467
6650
|
'tier0-catalog': { zh: 'Tier-0 常驻目录(指引层)', en: 'Tier-0 catalog (index layer)' },
|
|
@@ -7064,9 +7247,10 @@ function WorkspaceTab() {
|
|
|
7064
7247
|
// 2026-09-22 补接线(设置页缺口审计 P0-1):procedureInjectEnabled 才是宿主真读的总闸
|
|
7065
7248
|
// (procedure-switch.js 先判它);此前设置页只有旧键 ⇒ 用户改了也无效(灰控件)。
|
|
7066
7249
|
field(t('fProcInject'), h('input', { type: 'checkbox', 'data-dam-key': 'procedureInjectEnabled', checked: cfg.procedureInjectEnabled !== false, onChange: function (e) { set('procedureInjectEnabled', e.target.checked) } }), t('fProcInjectHint')),
|
|
7067
|
-
// ★
|
|
7068
|
-
//
|
|
7069
|
-
|
|
7250
|
+
// ★A-1(2026-09-23):此处原有「机械流程切片」复选框 —— 该功能已整条删除
|
|
7251
|
+
// (用户裁定「机械通路已被我弃用」,memory-hub.js 分支 + index.js getter 同步移除)。
|
|
7252
|
+
// 控件一并删掉:留着它会「写盘成功但界面无变化」(读到的键已无消费者)——
|
|
7253
|
+
// 正是用户明确列为「功能坏了」的那种表现。
|
|
7070
7254
|
field(t('fProcLevel'), h('select', { 'data-dam-select': '', value: cfg.procedureActiveLevel || 'checklist', onChange: function (e) { set('procedureActiveLevel', e.target.value) } },
|
|
7071
7255
|
h('option', { value: 'checklist' }, locale === 'zh' ? 'checklist 完整步骤' : 'checklist (full steps)'),
|
|
7072
7256
|
h('option', { value: 'excerpt' }, locale === 'zh' ? 'excerpt 摘要' : 'excerpt (summary)'),
|
|
@@ -7172,7 +7356,7 @@ function WorkspaceTab() {
|
|
|
7172
7356
|
mdlOpen ? buildModelDrawer() : null,
|
|
7173
7357
|
]),
|
|
7174
7358
|
section('about', sectionLabels.about, [
|
|
7175
|
-
// WB-GRAPH 白板线一键切换(2026-09-16,
|
|
7359
|
+
// WB-GRAPH 白板线一键切换(2026-09-16, board_mode_pre_v1): 旧版白板(默认) / 新版看板(dsh-graph vendor
|
|
7176
7360
|
// + sidecar + 遍历工具, 工具数 14→16)。开关解耦: 只管白板线; 切换需重启 dsh web 生效(工具注册在启动期)。
|
|
7177
7361
|
field(locale === 'zh' ? '白板模式(旧版白板 / 新版看板)' : 'Board mode (legacy / graph)', (function () {
|
|
7178
7362
|
var cur = cfg.boardMode === 'graph' ? 'graph' : 'legacy'
|
|
@@ -7468,19 +7652,19 @@ function WorkspaceTab() {
|
|
|
7468
7652
|
function registerSurfaces() {
|
|
7469
7653
|
try {
|
|
7470
7654
|
surfaceDisposers.push(slots.inject('sidebar.footer.action', function () {
|
|
7471
|
-
return slots.register({ name: 'sidebar.footer.action', id: 'auto-memory', order: 5, label: t('memory') + ' (pre)' }, function () { return h(SidebarButton) })
|
|
7655
|
+
return slots.register({ name: 'sidebar.footer.action', id: 'auto-memory-pre', order: 5, label: t('memory') + ' (pre)' }, function () { return h(SidebarButton) })
|
|
7472
7656
|
}))
|
|
7473
7657
|
surfaceDisposers.push(slots.inject('shell.overlay', function () {
|
|
7474
|
-
return slots.register({ name: 'shell.overlay', id: 'auto-memory', order: 5 }, function () { return h(MemoryPanel) })
|
|
7658
|
+
return slots.register({ name: 'shell.overlay', id: 'auto-memory-pre', order: 5 }, function () { return h(MemoryPanel) })
|
|
7475
7659
|
}))
|
|
7476
7660
|
surfaceDisposers.push(slots.inject('shell.overlay', function () {
|
|
7477
|
-
return slots.register({ name: 'shell.overlay', id: 'auto-memory-dialogs', order: 6 }, function () { return h(DialogHost) })
|
|
7661
|
+
return slots.register({ name: 'shell.overlay', id: 'auto-memory-pre-dialogs', order: 6 }, function () { return h(DialogHost) })
|
|
7478
7662
|
}))
|
|
7479
7663
|
surfaceDisposers.push(slots.inject('shell.overlay', function () {
|
|
7480
|
-
return slots.register({ name: 'shell.overlay', id: 'auto-memory-autocont', order: 7 }, function () { return h(AutoContinueHost) })
|
|
7664
|
+
return slots.register({ name: 'shell.overlay', id: 'auto-memory-pre-autocont', order: 7 }, function () { return h(AutoContinueHost) })
|
|
7481
7665
|
}))
|
|
7482
7666
|
surfaceDisposers.push(slots.inject('settings.section', function () {
|
|
7483
|
-
return slots.register({ name: 'settings.section', id: 'auto-memory', order: 25, label: t('autoMemory') + ' (pre)' }, function (props) { return h(SettingsPage, { close: props && props.close }) })
|
|
7667
|
+
return slots.register({ name: 'settings.section', id: 'auto-memory-pre', order: 25, label: t('autoMemory') + ' (pre)' }, function (props) { return h(SettingsPage, { close: props && props.close }) })
|
|
7484
7668
|
}))
|
|
7485
7669
|
// ★2026-09-21 二次修正:「记忆」会话页 —— 与「对话轨迹」「上下文」「白板看板」并列的一页。
|
|
7486
7670
|
// 用户实测反馈:「它现在是浮在整个页面上方的,而不是和对话轨迹、上下文、白板看板在一起,作为一个单独的一页」。
|
|
@@ -7490,7 +7674,7 @@ function WorkspaceTab() {
|
|
|
7490
7674
|
if (controller.panelPos() === 'page' || controller.panelPos() === 'both') {
|
|
7491
7675
|
surfaceDisposers.push(slots.inject('conversation.view', function () {
|
|
7492
7676
|
return slots.register(
|
|
7493
|
-
{ name: 'conversation.view', id: 'auto-memory-panel', order: 78, label: function () { return locale === 'zh' ? '记忆' : 'Memory' } },
|
|
7677
|
+
{ name: 'conversation.view', id: 'auto-memory-pre-panel', order: 78, label: function () { return locale === 'zh' ? '记忆' : 'Memory' } },
|
|
7494
7678
|
function (props) { return h(MemoryPageView, props || {}) },
|
|
7495
7679
|
)
|
|
7496
7680
|
}))
|
|
@@ -7501,7 +7685,7 @@ function WorkspaceTab() {
|
|
|
7501
7685
|
// 不改变 legacy 档下顶栏是否出现该 Tab 的行为(那由宿主/开关层决定, 本处不加判断)。
|
|
7502
7686
|
surfaceDisposers.push(slots.inject('conversation.view', function () {
|
|
7503
7687
|
return slots.register(
|
|
7504
|
-
{ name: 'conversation.view', id: 'auto-memory-kanban', order: 80, label: function () { return locale === 'zh' ? '白板看板' : 'Whiteboard' } },
|
|
7688
|
+
{ name: 'conversation.view', id: 'auto-memory-pre-kanban', order: 80, label: function () { return locale === 'zh' ? '白板看板' : 'Whiteboard' } },
|
|
7505
7689
|
function (props) { return h(KanbanView, props || {}) },
|
|
7506
7690
|
)
|
|
7507
7691
|
}))
|
|
@@ -7521,7 +7705,7 @@ function WorkspaceTab() {
|
|
|
7521
7705
|
if (WBG_ENABLED) {
|
|
7522
7706
|
surfaceDisposers.push(slots.inject('conversation.view', function () {
|
|
7523
7707
|
return slots.register(
|
|
7524
|
-
{ name: 'conversation.view', id: 'auto-memory-graph', order: 81, label: function () { return locale === 'zh' ? '白板画布' : 'Graph' } },
|
|
7708
|
+
{ name: 'conversation.view', id: 'auto-memory-pre-graph', order: 81, label: function () { return locale === 'zh' ? '白板画布' : 'Graph' } },
|
|
7525
7709
|
function (props) { return h(WhiteboardGraphView, props || {}) },
|
|
7526
7710
|
)
|
|
7527
7711
|
}))
|
package/lib/config-io.js
CHANGED
|
@@ -32,7 +32,7 @@ import { setTimeout as sleepDelayPre } from 'node:timers/promises'
|
|
|
32
32
|
import path from 'node:path'
|
|
33
33
|
|
|
34
34
|
/** 临时文件后缀。与既有 hub 落盘的 `.tmp` 同族,便于排查时一眼认出。 */
|
|
35
|
-
export const
|
|
35
|
+
export const ATOMIC_TMP_SUFFIX_PRE_V1 = '.tmp'
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* ★2026-09-22(并发写修复):tmp 路径的**唯一段** = pid + 进程内自增序号。
|
|
@@ -41,7 +41,7 @@ export const ATOMIC_TMP_SUFFIX_V1 = '.tmp'
|
|
|
41
41
|
* 先完成者把 tmp 改名走,后到者的 rename 找不到源文件而抛 **ENOENT**(非瞬时错误、
|
|
42
42
|
* 按纪律不得重试)⇒ 后一次保存静默失败(只回 `{ok:false}`,界面看起来"改了没生效")。
|
|
43
43
|
* 实测复现:对同一目标并发 5 次 `writeTextAtomicPre`,部分调用返回
|
|
44
|
-
* `{ok:false, error:'ENOENT … rename … .tmp -> …'}`(见 `tests/smoke/smoke-test-p3-batchb-guards
|
|
44
|
+
* `{ok:false, error:'ENOENT … rename … .tmp -> …'}`(见 `tests/smoke/smoke-test-p3-batchb-guards.mjs` P3B-2d)。
|
|
45
45
|
*
|
|
46
46
|
* 处方:每次调用生成一个独立的 tmp 名 —— 同进程并发各写各的,跨进程并发靠 pid 区分;
|
|
47
47
|
* 最后一次 rename 胜出,**原子性不变量不变**(目标任意时刻仍是旧完整版或新完整版)。
|
|
@@ -56,7 +56,7 @@ function tmpUniqueSuffixPre() {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/** 损坏留存的默认后缀前缀。 */
|
|
59
|
-
export const
|
|
59
|
+
export const CORRUPT_QUARANTINE_PREFIX_PRE_V1 = '.corrupt-'
|
|
60
60
|
|
|
61
61
|
/** 时间戳串(用于损坏留存命名)。绝不影响主流程。 */
|
|
62
62
|
function tsPre(now) {
|
|
@@ -76,7 +76,7 @@ function tsPre(now) {
|
|
|
76
76
|
*/
|
|
77
77
|
export function quarantineFilePreSync(file, opts = {}) {
|
|
78
78
|
try {
|
|
79
|
-
const suffix = (opts && opts.suffix) || (
|
|
79
|
+
const suffix = (opts && opts.suffix) || (CORRUPT_QUARANTINE_PREFIX_PRE_V1 + tsPre(opts && opts.now))
|
|
80
80
|
const dest = file + suffix
|
|
81
81
|
// 同一秒内坏两次 ⇒ 再加一段随机后缀,避免覆盖上一份证据
|
|
82
82
|
const finalPath = existsSync(dest) ? dest + '.' + Math.random().toString(36).slice(2, 6) : dest
|
|
@@ -92,7 +92,7 @@ export function quarantineFilePreSync(file, opts = {}) {
|
|
|
92
92
|
* @returns {{ok: boolean, path?: string, error?: string}} 结构化结果(fail-soft,不抛)。
|
|
93
93
|
*/
|
|
94
94
|
export function writeTextAtomicPreSync(file, text) {
|
|
95
|
-
const tmp = file +
|
|
95
|
+
const tmp = file + ATOMIC_TMP_SUFFIX_PRE_V1 + tmpUniqueSuffixPre()
|
|
96
96
|
try {
|
|
97
97
|
mkdirSync(path.dirname(file), { recursive: true })
|
|
98
98
|
writeFileSync(tmp, String(text), 'utf8')
|
|
@@ -113,22 +113,22 @@ export function writeTextAtomicPreSync(file, text) {
|
|
|
113
113
|
* 处方:仅对这三个**瞬时**错误码退避重试;首项延迟为 0 ⇒ **正常路径零延迟变化**。
|
|
114
114
|
*
|
|
115
115
|
* ⚠️ **为什么不 import `fs-retry.js`**:本模块的**零运行时依赖纪律**(见文件头)
|
|
116
|
-
* 被 `tests/smoke/smoke-test-t8-configio
|
|
116
|
+
* 被 `tests/smoke/smoke-test-t8-configio.mjs` 的 T8c-6 钉死 —— 配置 I/O 层不得因为
|
|
117
117
|
* 任何兄弟模块的加载期故障而失效。故此处**内联**同一套退避刻度(只用 node: 内置),
|
|
118
118
|
* 语义与 `fs-retry.js` 的 `retryRename` 保持一致。
|
|
119
119
|
*/
|
|
120
|
-
const
|
|
121
|
-
const
|
|
120
|
+
const RENAME_RETRY_DELAYS_PRE_V1 = Object.freeze([0, 50, 150, 400, 1000])
|
|
121
|
+
const TRANSIENT_RENAME_CODES_PRE_V1 = new Set(['EPERM', 'EACCES', 'EBUSY'])
|
|
122
122
|
async function renameBoundedRetryPre(from, to) {
|
|
123
|
-
const last =
|
|
123
|
+
const last = RENAME_RETRY_DELAYS_PRE_V1.length - 1
|
|
124
124
|
for (let attempt = 0; attempt <= last; attempt++) {
|
|
125
|
-
const wait =
|
|
125
|
+
const wait = RENAME_RETRY_DELAYS_PRE_V1[attempt]
|
|
126
126
|
if (wait) await sleepDelayPre(wait)
|
|
127
127
|
try {
|
|
128
128
|
await rename(from, to)
|
|
129
129
|
return
|
|
130
130
|
} catch (e) {
|
|
131
|
-
const transient =
|
|
131
|
+
const transient = TRANSIENT_RENAME_CODES_PRE_V1.has(e && e.code)
|
|
132
132
|
if (!transient || attempt === last) throw e
|
|
133
133
|
}
|
|
134
134
|
}
|
|
@@ -143,7 +143,7 @@ async function renameBoundedRetryPre(from, to) {
|
|
|
143
143
|
* 同步版 `writeTextAtomicPreSync` 的契约是同步,保持 `renameSync` 不动。
|
|
144
144
|
*/
|
|
145
145
|
export async function writeTextAtomicPre(file, text) {
|
|
146
|
-
const tmp = file +
|
|
146
|
+
const tmp = file + ATOMIC_TMP_SUFFIX_PRE_V1 + tmpUniqueSuffixPre()
|
|
147
147
|
try {
|
|
148
148
|
mkdirSync(path.dirname(file), { recursive: true })
|
|
149
149
|
await writeFile(tmp, String(text), 'utf8')
|