@a9i5k4/dsh-auto-memory 2.3.1 → 2.4.0
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 +83 -10
- package/lib/context-host.js +22 -4
- package/lib/index.js +630 -327
- package/lib/python-setup.js +67 -14
- package/lib/water-window.js +59 -2
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -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}。改坏了可一键恢复默认。',
|
|
@@ -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.',
|
|
@@ -886,6 +890,7 @@ window.__ModuleLoader__.load({
|
|
|
886
890
|
pyStatus: '/api/dsh-auto-memory/python-setup/status',
|
|
887
891
|
handoffState: '/api/dsh-auto-memory/handoff-state',
|
|
888
892
|
handoffContinue: '/api/dsh-auto-memory/handoff-continue',
|
|
893
|
+
handoffPermission: '/api/dsh-auto-memory/handoff-permission',
|
|
889
894
|
autoContState: '/api/dsh-auto-memory/auto-continue-state',
|
|
890
895
|
autoContDecide: '/api/dsh-auto-memory/auto-continue-decide',
|
|
891
896
|
semanticDownload: '/api/dsh-auto-memory/semantic-download',
|
|
@@ -1478,7 +1483,7 @@ window.__ModuleLoader__.load({
|
|
|
1478
1483
|
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
1484
|
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
1485
|
})
|
|
1481
|
-
var allReady = data.venvOk && data.depsOk && data.modelReady
|
|
1486
|
+
var allReady = data.venvOk && data.depsOk && data.modelReady && data.configOk
|
|
1482
1487
|
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
1488
|
h('div', { style: { fontWeight: 700, marginBottom: '4px' } }, t('pyWizTitle')),
|
|
1484
1489
|
h('div', { style: { opacity: .6, fontSize: 'calc(10.5px * var(--dam-scale))', marginBottom: '6px' } }, t('pyWizModelHint')),
|
|
@@ -2012,6 +2017,10 @@ window.__ModuleLoader__.load({
|
|
|
2012
2017
|
// 流程:③刷新仪式(旧 Agent 先刷 PLAN+账本)→ 取材料 → 修A(workspaceId 建会话)→ 序号标题
|
|
2013
2018
|
// → 修B(沿用模型+思考档位)→ 打开 → 注入材料。
|
|
2014
2019
|
var continueFlowBusy = false
|
|
2020
|
+
// 刷新仪式认到的旧会话 id(2026-09-10):权限继承要用它反查旧会话预设。
|
|
2021
|
+
// 材料里的 prevSessionId 依赖宿主 _lastAgent,重启后可能为空;这份来自 handoff-state.refresh,
|
|
2022
|
+
// 宿主侧带磁盘回退,更可靠。
|
|
2023
|
+
var lastRefreshSessionId = ''
|
|
2015
2024
|
function continueCreateArgs(d) {
|
|
2016
2025
|
// 修A(2026-09-08):官方 session.create 只接受 workspaceId 或 cwd 之一(同传报 bad-request),
|
|
2017
2026
|
// 且只有 workspaceId 分支会 workspace.attachSession()(api-session-controller/lib/index.js:566-588);
|
|
@@ -2021,18 +2030,31 @@ window.__ModuleLoader__.load({
|
|
|
2021
2030
|
}
|
|
2022
2031
|
async function refreshOldSession(onMsg) {
|
|
2023
2032
|
// ③接续前刷新仪式:让旧 Agent 把 PLAN.md 与交接账本刷到最新,host 再用最新材料组装交接。
|
|
2024
|
-
//
|
|
2033
|
+
// 2026-09-10:原先任何失败/超时都静默(fail-soft),用户看到的现象是"点了接续却什么也没发生、旧会话毫无动静"。
|
|
2034
|
+
// 现改为 fail-soft 但**可见** —— 跳过/失败/超时都回报一句原因,接续本身照常继续。
|
|
2025
2035
|
try {
|
|
2026
2036
|
var st = await apiGet(API.handoffState)
|
|
2027
2037
|
var rf = st && st.refresh
|
|
2028
|
-
if (!rf || !rf.sessionId || !rf.prompt)
|
|
2029
|
-
|
|
2038
|
+
if (!rf || !rf.sessionId || !rf.prompt) {
|
|
2039
|
+
if (onMsg) onMsg(locale === 'zh' ? '⚠ 刷新仪式已跳过:宿主未给出旧会话 id(刚重启、旧会话未活跃时会发生)——本次直接用现有材料接续。' : '⚠ Refresh ritual skipped: host supplied no old-session id (happens right after a restart) — continuing with current material.')
|
|
2040
|
+
return { ok: false, skipped: true }
|
|
2041
|
+
}
|
|
2042
|
+
if (!remoteFace || !remoteFace.session || typeof remoteFace.session.prompt !== 'function') {
|
|
2043
|
+
if (onMsg) onMsg(locale === 'zh' ? '⚠ 刷新仪式已跳过:harness 未提供 remote.session。' : '⚠ Refresh ritual skipped: remote.session unavailable.')
|
|
2044
|
+
return { ok: false, skipped: true }
|
|
2045
|
+
}
|
|
2030
2046
|
var before = String((st.planMtime || 0)) + '|' + String((st.ledgers && st.ledgers[0] && st.ledgers[0].name) || '')
|
|
2047
|
+
lastRefreshSessionId = String(rf.sessionId || '')
|
|
2031
2048
|
if (onMsg) onMsg(locale === 'zh' ? '刷新仪式:请旧 Agent 更新白板 PLAN 与交接账本…' : 'Refresh ritual: asking the old agent to update PLAN + ledger…')
|
|
2032
2049
|
await remoteFace.session.prompt({ requestId: amReqId('amr'), sessionId: rf.sessionId, mode: 'queue', content: [{ type: 'text', text: rf.prompt }], clientTimeZone: amCtzValue() })
|
|
2033
2050
|
var waited = await waitForRefresh(rf.sessionId, before)
|
|
2051
|
+
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
2052
|
return { ok: true, waited: waited }
|
|
2035
|
-
} catch (eRf) {
|
|
2053
|
+
} catch (eRf) {
|
|
2054
|
+
var msg = String(eRf && eRf.message ? eRf.message : eRf)
|
|
2055
|
+
if (onMsg) onMsg(locale === 'zh' ? '⚠ 刷新仪式失败:' + msg + '(用现有材料继续)' : '⚠ Refresh ritual failed: ' + msg + ' (continuing with current material)')
|
|
2056
|
+
return { ok: false, error: msg }
|
|
2057
|
+
}
|
|
2036
2058
|
}
|
|
2037
2059
|
async function waitForRefresh(oldId, before) {
|
|
2038
2060
|
// 结束条件(任一):材料变了 / 旧会话跑完这一轮(true→false) / 超时(默认 90s,配置可调)。
|
|
@@ -2079,7 +2101,28 @@ window.__ModuleLoader__.load({
|
|
|
2079
2101
|
try { await rf.session.selectModel({ sessionId: newId, provider: d.provider, model: d.model, reasoningEffort: d.reasoningEffort || undefined }) } catch (eMdl) {}
|
|
2080
2102
|
}
|
|
2081
2103
|
try { if (sessions && typeof sessions.open === 'function') sessions.open(newId) } catch (eOpen) {}
|
|
2104
|
+
// 权限继承(2026-09-10):官方 session.create 不收权限字段,新会话一律落 settings 的
|
|
2105
|
+
// permission.defaultPreset(本机默认曾=workspace-write+ask)→ 接续后每一步都要用户批准,静默运行被打断。
|
|
2106
|
+
// 两段式:投料前先试(命中则**首轮就已继承**),投料后再试(新会话的 agent 往往是投料时才建出来)。
|
|
2107
|
+
var fromSid = String((d && d.prevSessionId) || lastRefreshSessionId || '')
|
|
2108
|
+
var permPreset = ''
|
|
2109
|
+
if (fromSid) {
|
|
2110
|
+
try {
|
|
2111
|
+
var pp1 = await apiPost(API.handoffPermission, { fromSessionId: fromSid, toSessionId: newId, attempts: [0, 400] })
|
|
2112
|
+
if (pp1 && pp1.ok) permPreset = String(pp1.preset || '')
|
|
2113
|
+
} catch (ePp1) {}
|
|
2114
|
+
}
|
|
2082
2115
|
await rf.session.prompt({ requestId: amReqId('amc'), sessionId: newId, mode: 'queue', content: [{ type: 'text', text: d.carryText }], clientTimeZone: amCtzValue() })
|
|
2116
|
+
if (!permPreset && fromSid) {
|
|
2117
|
+
try {
|
|
2118
|
+
var pp2 = await apiPost(API.handoffPermission, { fromSessionId: fromSid, toSessionId: newId, attempts: [0, 700, 1600] })
|
|
2119
|
+
if (pp2 && pp2.ok) permPreset = String(pp2.preset || '')
|
|
2120
|
+
else if (onMsg && pp2 && pp2.reason && pp2.reason !== 'no-permission-service') {
|
|
2121
|
+
onMsg(locale === 'zh' ? '⚠ 权限未继承(' + pp2.reason + '),新会话按设置里的默认权限运行。' : '⚠ Permission not inherited (' + pp2.reason + '); new session runs with the default permission.')
|
|
2122
|
+
}
|
|
2123
|
+
} catch (ePp2) {}
|
|
2124
|
+
}
|
|
2125
|
+
if (onMsg && permPreset) onMsg(locale === 'zh' ? '已继承旧会话权限:' + permPreset : 'Permission inherited: ' + permPreset)
|
|
2083
2126
|
return newId
|
|
2084
2127
|
}
|
|
2085
2128
|
async function runContinueFlow(opts) {
|
|
@@ -2093,7 +2136,8 @@ window.__ModuleLoader__.load({
|
|
|
2093
2136
|
var ritual = !(cfg && cfg.autoContinueRefreshRitual === false) && !(opts && opts.skipRefresh)
|
|
2094
2137
|
if (ritual) { try { await refreshOldSession(onMsg) } catch (eRf) {} }
|
|
2095
2138
|
onMsg(locale === 'zh' ? '正在构造交接材料(含旧会话转写)…' : 'Preparing handoff material (incl. prev-session transcript)…')
|
|
2096
|
-
var
|
|
2139
|
+
var fromSidForCarry = String(lastRefreshSessionId || currentSessionIdClient() || '')
|
|
2140
|
+
var d = await apiPost(API.handoffContinue, fromSidForCarry ? { fromSessionId: fromSidForCarry } : {})
|
|
2097
2141
|
if (!d || !d.ok) throw new Error((d && d.error) || 'no handoff material')
|
|
2098
2142
|
var newId = await executeContinue(d, onMsg)
|
|
2099
2143
|
try { localStorage.setItem('dsh-auto-memory.autoCont.last', String(Date.now())) } catch (eL) {}
|
|
@@ -3737,6 +3781,29 @@ window.__ModuleLoader__.load({
|
|
|
3737
3781
|
}
|
|
3738
3782
|
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
3783
|
}
|
|
3784
|
+
// 思考强度(DSH 0.1.5 agentOptions.reasoningEffort):与模型成对下发给子代理;留空=跟随模型默认。
|
|
3785
|
+
// 合法值按 DeepSeek 适配器口径 off|low|high|max(默认 high)——服务端同样做白名单过滤。
|
|
3786
|
+
var effortOpts = [
|
|
3787
|
+
['', locale === 'zh' ? '跟随默认' : 'default'],
|
|
3788
|
+
['off', 'off'],
|
|
3789
|
+
['low', 'low'],
|
|
3790
|
+
['high', 'high'],
|
|
3791
|
+
['max', 'max'],
|
|
3792
|
+
]
|
|
3793
|
+
var curEffort = String(cfg.subagentReasoningEffort || '').toLowerCase()
|
|
3794
|
+
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)' } },
|
|
3795
|
+
h('b', { style: { display: 'block', fontSize: 'calc(12px * var(--dam-scale))' } }, locale === 'zh' ? '思考强度(子代理)' : 'Reasoning effort (subagents)'),
|
|
3796
|
+
h('div', { 'data-dam-row': '', style: { flexWrap: 'wrap', marginTop: '4px' } },
|
|
3797
|
+
effortOpts.map(function (o) {
|
|
3798
|
+
return h('button', {
|
|
3799
|
+
key: 'eff-' + (o[0] || 'def'), 'data-dam-btn': '',
|
|
3800
|
+
style: { padding: '3px 9px', fontWeight: curEffort === o[0] ? 700 : 400, opacity: curEffort === o[0] ? 1 : 0.72 },
|
|
3801
|
+
onClick: function () { set('subagentReasoningEffort', o[0]) },
|
|
3802
|
+
}, o[1] + (curEffort === o[0] ? ' ✓' : ''))
|
|
3803
|
+
})),
|
|
3804
|
+
h('div', { 'data-dam-hint': '' }, locale === 'zh'
|
|
3805
|
+
? 'off=关闭思维链,low/high/max 递增思考深度(DeepSeek 默认 high)。只作用于本插件的子代理(时段总结/问候/自动沉淀/蒸馏),不影响主对话;留空或模型不支持时跟随模型默认。'
|
|
3806
|
+
: '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
3807
|
return h('div', { style: panelStyle }, kids)
|
|
3741
3808
|
}
|
|
3742
3809
|
var verPair = useState(null)
|
|
@@ -3922,6 +3989,10 @@ window.__ModuleLoader__.load({
|
|
|
3922
3989
|
h('option', { value: 'js' }, t('semJs')),
|
|
3923
3990
|
h('option', { value: 'python' }, t('semPy'))),
|
|
3924
3991
|
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 ? '⏳' : '⟳ 检测'),
|
|
3992
|
+
// 2026-09-10:向导入口原先只在"资产未就绪"时自动弹出,已装好的用户反而进不去(想重装/换布局无处可点)。
|
|
3993
|
+
// 现在常驻一个显式开关,永远可达;已就绪时打开也只是复用同一套下载/校验步骤。
|
|
3994
|
+
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')) } },
|
|
3995
|
+
guide ? (locale === 'zh' ? '收起向导' : 'Hide wizard') : (locale === 'zh' ? '🧩 安装向导' : '🧩 Setup')),
|
|
3925
3996
|
]), t('semModeHint')),
|
|
3926
3997
|
sem.loaded ? h('div', { 'data-dam-hint': '', style: { marginTop: '-4px', marginBottom: '8px' } },
|
|
3927
3998
|
t('semResolved') + ': ' + (sem.resolvedTier === 'c2' ? t('tierC2') + ' ✓' : sem.resolvedTier === 'c3' ? t('tierC3') + ' ✓' : t('tierC1')))
|
|
@@ -4103,6 +4174,8 @@ window.__ModuleLoader__.load({
|
|
|
4103
4174
|
: null]),
|
|
4104
4175
|
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
4176
|
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')),
|
|
4177
|
+
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')),
|
|
4178
|
+
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
4179
|
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
4180
|
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
4181
|
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 +4209,10 @@ window.__ModuleLoader__.load({
|
|
|
4136
4209
|
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
4210
|
field(t('fStyle'), h('select', { 'data-dam-select': '', value: cfg.reflectStyle, onChange: function (e) { set('reflectStyle', e.target.value) } },
|
|
4138
4211
|
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' ? '
|
|
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' ? '
|
|
4212
|
+
field(locale === 'zh' ? '子代理模型 / 思考强度' : 'Subagent model & reasoning effort', h('div', { 'data-dam-row': '', style: { flex: 1 } },
|
|
4213
|
+
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) : '')),
|
|
4214
|
+
h('button', { 'data-dam-btn': '', onClick: openModels }, locale === 'zh' ? '选择模型 / 强度' : 'Pick model / effort')),
|
|
4215
|
+
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
4216
|
mdlOpen ? buildModelDrawer() : null,
|
|
4144
4217
|
]),
|
|
4145
4218
|
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')),
|
package/lib/context-host.js
CHANGED
|
@@ -29,6 +29,10 @@ import { fuseD6Pre } from './semantic-js.js'
|
|
|
29
29
|
import { buildSourceCatalog, loadCorpusSnapshot, CorpusRegistry, canonicalize } from './m4-corpus.js'
|
|
30
30
|
|
|
31
31
|
const MAX_DROPS_RING = 64
|
|
32
|
+
/** 同一 drop 原因的日志限流窗口(ms):窗口内只写首条,其余计数。防会话回放刷屏(2026-09-10)。 */
|
|
33
|
+
const DROP_LOG_WINDOW_MS = 30000
|
|
34
|
+
const dropLoggedAt = new Map()
|
|
35
|
+
const dropLoggedSkip = new Map()
|
|
32
36
|
|
|
33
37
|
/** M7.5 诊断:ctx-host drop/skip 直写 harness diagnose 日志(此前 try{diag()} 静默吞 ReferenceError)。 */
|
|
34
38
|
function diagCtx(msg) {
|
|
@@ -163,10 +167,24 @@ export function createContextHost(opts = {}) {
|
|
|
163
167
|
if (!st) { st = { lastPushedContextVersion: -1, disposed: false }; states.set(runtime, st) }
|
|
164
168
|
return st
|
|
165
169
|
}
|
|
166
|
-
function pushDrop(reason, contextVersion) {
|
|
170
|
+
function pushDrop(reason, contextVersion, runtimeKey) {
|
|
167
171
|
volatileDrops.push({ at: Date.now(), reason, contextVersion })
|
|
168
172
|
if (volatileDrops.length > MAX_DROPS_RING) volatileDrops.shift()
|
|
169
|
-
|
|
173
|
+
// 回放风暴限流(2026-09-10):DSH 载入/观测一个历史会话时会对每一段历史都调一次 accept,
|
|
174
|
+
// 而该 runtime 从未 capturePaths → 同一原因一次性刷出上百行(实测单次回放 288 行,占诊断日志近半)。
|
|
175
|
+
// 同因日志按时间窗限流:窗口内只写首条,首条附 runtime key(定位是哪个会话),其余仅计数。
|
|
176
|
+
const _k = String(reason)
|
|
177
|
+
const _now = Date.now()
|
|
178
|
+
if (_now - (dropLoggedAt.get(_k) || 0) < DROP_LOG_WINDOW_MS) {
|
|
179
|
+
dropLoggedSkip.set(_k, (dropLoggedSkip.get(_k) || 0) + 1)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
dropLoggedAt.set(_k, _now)
|
|
183
|
+
const _skip = dropLoggedSkip.get(_k) || 0
|
|
184
|
+
dropLoggedSkip.set(_k, 0)
|
|
185
|
+
diagCtx('ctx-host drop: ' + reason + ' cv=' + contextVersion
|
|
186
|
+
+ (runtimeKey ? ' key=' + String(runtimeKey).slice(0, 48) : '')
|
|
187
|
+
+ (_skip ? ' (+' + _skip + ' suppressed in ' + (DROP_LOG_WINDOW_MS / 1000) + 's)' : ''))
|
|
170
188
|
}
|
|
171
189
|
function capturePaths(runtimeKey, p) {
|
|
172
190
|
pathsByKey.set(String(runtimeKey || ''), {
|
|
@@ -290,7 +308,7 @@ export function createContextHost(opts = {}) {
|
|
|
290
308
|
// M7-8 live-parity 诊断:记录最近 Segment 的 runtime key/sessionRef(最小投影)
|
|
291
309
|
lastSegmentRuntimeKey = String(runtime.key || '')
|
|
292
310
|
lastSegmentSessionRef = String(runtime.sessionId || '').slice(0, 24)
|
|
293
|
-
if (!paths) { pushDrop('no-paths-captured', seg.contextVersion); return }
|
|
311
|
+
if (!paths) { pushDrop('no-paths-captured', seg.contextVersion, runtime.key); return }
|
|
294
312
|
const corpusSnap = loadCorpus(paths)
|
|
295
313
|
// ---- cite / correction(user+assistant 可见文本;precision-first,需完整 token+provenance)----
|
|
296
314
|
if ((seg.kind === 'user' || seg.kind === 'assistant') && seg.text && corpusSnap && corpusSnap.records.length) {
|
|
@@ -331,7 +349,7 @@ export function createContextHost(opts = {}) {
|
|
|
331
349
|
void Promise.all([readyPromise, c2RankPromise]).then(([readyRes, rank]) => {
|
|
332
350
|
if (!(readyRes && readyRes.ready)) {
|
|
333
351
|
stats.pushesRejected++
|
|
334
|
-
pushDrop('index-not-ready:' + ((readyRes && readyRes.reason) || 'unknown'), runtime.contextVersion)
|
|
352
|
+
pushDrop('index-not-ready:' + ((readyRes && readyRes.reason) || 'unknown'), runtime.contextVersion, runtime.key)
|
|
335
353
|
return
|
|
336
354
|
}
|
|
337
355
|
if (st.disposed || runtime.disposed) return
|