@a9i5k4/dsh-auto-memory 0.1.28 → 0.1.30
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/README.md +352 -266
- package/README.zh-CN.md +370 -264
- package/cordis.patch.yml +9 -9
- package/lib/activation-host.js +455 -0
- package/lib/activation-inbox-state.js +261 -0
- package/lib/activation-inbox.js +426 -0
- package/lib/client.js +1395 -50
- package/lib/context-bridge.js +619 -0
- package/lib/context-host.js +712 -0
- package/lib/context-sink-python.js +90 -0
- package/lib/episodic-store.js +316 -0
- package/lib/evidence-store.js +272 -0
- package/lib/fact-store.js +418 -0
- package/lib/index-sync.js +160 -0
- package/lib/index.js +2386 -105
- package/lib/intent-clean.js +74 -0
- package/lib/m4-corpus.js +169 -0
- package/lib/m7-index-sync-host.js +194 -0
- package/lib/m7-wire.js +268 -0
- package/lib/memory-anchor.js +451 -0
- package/lib/memory-hub.js +259 -0
- package/lib/memory-index.js +145 -0
- package/lib/memory-writer.js +391 -0
- package/lib/policies/activation_policy_v2.json +88 -0
- package/lib/policies/recall_intent_lr_v1.json +1 -0
- package/lib/procedure-store.js +406 -0
- package/lib/python-sidecar-client.js +326 -0
- package/lib/semantic-decide.js +265 -0
- package/lib/semantic-js.js +381 -0
- package/lib/shadow-host.js +361 -0
- package/lib/shadow-retrieval.js +673 -0
- package/lib/storage-manage.js +203 -0
- package/package.json +2 -2
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M5-3 Context Bridge Host Wiring(docs/M5-CONTRACT.md §12 M5-3)。
|
|
3
|
+
* 桥接 lib/index.js(M2 SessionRuntime/事件流)与 M5-1 纯核心+M5-2 Store:
|
|
4
|
+
* - per-runtime state(WeakMap,lazy;enableEpoch/lastPushedContextVersion/inflight)
|
|
5
|
+
* - accepted Segment → 组装 ContextPushEnvelopePre(trigger/window/memoryRefs/aggregates)
|
|
6
|
+
* → Null/Fake sink(按 config.contextSinkMode 切换)→ push bridge(幂等/latest-wins/abort)
|
|
7
|
+
* - read coverage/cite/correction → AccessEvidencePre → EvidenceEventStore(隐私投影落盘)
|
|
8
|
+
* - memoryRefs 选择复用 M4 lexical_v2 可解释基线与 m4-corpus 授权校验链;
|
|
9
|
+
* M4 Shadow audit 候选绝不追认为 seen/read(本模块不读 shadow audit,结构隔离)。
|
|
10
|
+
* 默认关闭(associativeMemoryEnabled && contextBridgeEnabled 双门):零构造、零 IO、零留存。
|
|
11
|
+
* 本模块自身无 spawn/HTTP;contextSinkMode='python' 仅在 assoc∧bridge∧pythonBackend 三重门下
|
|
12
|
+
* 经 lib/context-sink-python.js 使用共享 SidecarClient(M7-1),否则回退 null sink。
|
|
13
|
+
* UTF-8 无 BOM。
|
|
14
|
+
*/
|
|
15
|
+
import { appendFileSync, existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
16
|
+
import { createHash } from 'node:crypto'
|
|
17
|
+
import path from 'node:path'
|
|
18
|
+
import {
|
|
19
|
+
buildContextPushEnvelopePre, buildAuthorizedMemoryRefFromRecord, createAccessEvidencePre,
|
|
20
|
+
createCiteEvidencesFromText, createCorrectionEvidencesFromText, computeReadCoverage,
|
|
21
|
+
createContextPushBridge, createNullContextSinkPre, createFakeContextSinkPre,
|
|
22
|
+
CONTEXT_BRIDGE_BUDGET_V1, CONTEXT_BRIDGE_POLICY_VERSION, EVIDENCE_POLICY_VERSION,
|
|
23
|
+
} from './context-bridge.js'
|
|
24
|
+
import { EvidenceEventStore, rebuildAggregates, workspaceRefOf } from './evidence-store.js'
|
|
25
|
+
import { createPythonContextSinkPre } from './context-sink-python.js'
|
|
26
|
+
import { buildQueryPlan, lexicalSearch, GATE_POLICY_VERSION, LEXICAL_POLICY_VERSION } from './shadow-retrieval.js'
|
|
27
|
+
import { fuseD6Pre } from './semantic-js.js'
|
|
28
|
+
import { buildSourceCatalog, loadCorpusSnapshot, CorpusRegistry, canonicalize } from './m4-corpus.js'
|
|
29
|
+
|
|
30
|
+
const MAX_DROPS_RING = 64
|
|
31
|
+
|
|
32
|
+
/** M7.5 诊断:ctx-host drop/skip 直写 harness diagnose 日志(此前 try{diag()} 静默吞 ReferenceError)。 */
|
|
33
|
+
function diagCtx(msg) {
|
|
34
|
+
try {
|
|
35
|
+
const env = process.env.DSH_HOME
|
|
36
|
+
const base = env && env.trim() ? env.trim()
|
|
37
|
+
: (process.env.USERPROFILE || process.env.HOME || '')
|
|
38
|
+
if (!base) return
|
|
39
|
+
appendFileSync(path.join(base, '.dsh', 'dsh-auto-memory-diagnose.log'),
|
|
40
|
+
new Date().toISOString() + ' [ctx-host] ' + String(msg).slice(0, 300) + '\n', 'utf8')
|
|
41
|
+
} catch (e) {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createContextHost(opts = {}) {
|
|
45
|
+
const engine = opts.engine
|
|
46
|
+
if (!engine) throw new Error('context-host: engine required')
|
|
47
|
+
const states = new WeakMap() // runtime → state(lazy)
|
|
48
|
+
const volatileDrops = [] // ≤16 条最小投影(无文本)
|
|
49
|
+
const stats = { envelopesBuilt: 0, pushesAccepted: 0, pushesRejected: 0, superseded: 0,
|
|
50
|
+
evidenceAppended: 0, evidenceDuplicates: 0, evidenceFailed: 0, readsCovered: 0, stalesSeen: 0, errors: 0,
|
|
51
|
+
// 2026-08-27 JS 判定观测(真实数据验证):按段类型计数判定/emit/shadow 拦截,经 debugView 暴露
|
|
52
|
+
jsDecideRuns: 0, jsDecideEmits: 0, jsDecideShadowed: 0,
|
|
53
|
+
jsDecideByKind: {}, // {user: n, reasoning: n, assistant: n, tool: n}
|
|
54
|
+
}
|
|
55
|
+
const pathsByKey = new Map()
|
|
56
|
+
let lastFrameIdentity = null // 最小投影:observationId 全值(本身是哈希)+计数,无任何文本
|
|
57
|
+
let lastSegmentRuntimeKey = null
|
|
58
|
+
let lastSegmentSessionRef = null
|
|
59
|
+
let lastSinkMode = null
|
|
60
|
+
let bridge = null
|
|
61
|
+
let store = null
|
|
62
|
+
let aggCache = null // {wsRef, miv, list}
|
|
63
|
+
const registry = new CorpusRegistry({ sidecarDir: path.join(dshHome(), 'memory', 'index', 'files') })
|
|
64
|
+
|
|
65
|
+
function dshHome() {
|
|
66
|
+
const env = process.env.DSH_HOME
|
|
67
|
+
if (env && env.trim()) return env.trim()
|
|
68
|
+
const base = engine.__homedirFn ? engine.__homedirFn() : (process.env.USERPROFILE || process.env.HOME || '')
|
|
69
|
+
return base ? path.join(base, '.dsh') : '.'
|
|
70
|
+
}
|
|
71
|
+
function effectiveEnabled() {
|
|
72
|
+
return engine.config.associativeMemoryEnabled === true && engine.config.contextBridgeEnabled === true
|
|
73
|
+
}
|
|
74
|
+
function pythonGate() {
|
|
75
|
+
// M7-1 三重门(PYTHON-SIDECAR-CONTRACT §13.1):默认 false/null 时恒为假 → 永远走 null sink
|
|
76
|
+
return engine.config.associativeMemoryEnabled === true &&
|
|
77
|
+
engine.config.contextBridgeEnabled === true &&
|
|
78
|
+
engine.config.pythonBackendEnabled === true
|
|
79
|
+
}
|
|
80
|
+
function sinkMode() {
|
|
81
|
+
const m = String(engine.config.contextSinkMode || 'null')
|
|
82
|
+
if (m === 'fake') return 'fake'
|
|
83
|
+
if (m === 'python' && pythonGate()) return 'python'
|
|
84
|
+
return 'null'
|
|
85
|
+
}
|
|
86
|
+
let pythonSink = null
|
|
87
|
+
function getPythonSink() {
|
|
88
|
+
if (!pythonSink) {
|
|
89
|
+
// 共享 engine 级 SidecarClient(lazy start);worker 主动 activation_request 帧上抛给现有
|
|
90
|
+
// M6 host(offerExternalActivation),本模块不构建 Packet、不改 M6 validator/delivery/seen。
|
|
91
|
+
pythonSink = createPythonContextSinkPre({
|
|
92
|
+
client: engine._pythonSidecar,
|
|
93
|
+
onActivation: (evt) => { try { if (engine._activationHost) engine._activationHost.offerExternalActivation(evt && evt.activation) } catch (_) {} },
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
return pythonSink
|
|
97
|
+
}
|
|
98
|
+
function bridgeFor() {
|
|
99
|
+
const mode = sinkMode()
|
|
100
|
+
if (!bridge || lastSinkMode !== mode) {
|
|
101
|
+
const sink = mode === 'fake'
|
|
102
|
+
? createFakeContextSinkPre({ capacity: 64 })
|
|
103
|
+
: (mode === 'python' ? getPythonSink() : createNullContextSinkPre())
|
|
104
|
+
bridge = createContextPushBridge({ sink })
|
|
105
|
+
lastSinkMode = mode
|
|
106
|
+
}
|
|
107
|
+
return bridge
|
|
108
|
+
}
|
|
109
|
+
function storeFor() {
|
|
110
|
+
if (!store) store = new EvidenceEventStore({ root: path.join(dshHome(), 'memory', 'evidence') })
|
|
111
|
+
return store
|
|
112
|
+
}
|
|
113
|
+
function evidenceRootExists() {
|
|
114
|
+
const p = path.join(dshHome(), 'memory', 'evidence')
|
|
115
|
+
try { return existsSync(p) } catch (_) { return false }
|
|
116
|
+
}
|
|
117
|
+
function stateFor(runtime) {
|
|
118
|
+
let st = states.get(runtime)
|
|
119
|
+
if (!st) { st = { lastPushedContextVersion: -1, disposed: false }; states.set(runtime, st) }
|
|
120
|
+
return st
|
|
121
|
+
}
|
|
122
|
+
function pushDrop(reason, contextVersion) {
|
|
123
|
+
volatileDrops.push({ at: Date.now(), reason, contextVersion })
|
|
124
|
+
if (volatileDrops.length > MAX_DROPS_RING) volatileDrops.shift()
|
|
125
|
+
diagCtx('ctx-host drop: ' + reason + ' cv=' + contextVersion)
|
|
126
|
+
}
|
|
127
|
+
function capturePaths(runtimeKey, p) {
|
|
128
|
+
pathsByKey.set(String(runtimeKey || ''), {
|
|
129
|
+
workspaceKey: canonicalize(p.ws),
|
|
130
|
+
userMemoryPath: p.userDir ? path.join(p.userDir, 'MEMORY.md') : undefined,
|
|
131
|
+
workspaceMemoryPath: p.notesPath,
|
|
132
|
+
todayLogPath: p.logPath,
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 加载当前工作区语料(fingerprint 缓存);失败返回 null(不阻塞 envelope 构造)。 */
|
|
137
|
+
function loadCorpus(paths) {
|
|
138
|
+
try {
|
|
139
|
+
const catalog = buildSourceCatalog({
|
|
140
|
+
workspaceKey: paths.workspaceKey,
|
|
141
|
+
userMemoryPath: paths.userMemoryPath,
|
|
142
|
+
workspaceMemoryPath: paths.workspaceMemoryPath,
|
|
143
|
+
todayLogPath: paths.todayLogPath,
|
|
144
|
+
})
|
|
145
|
+
const res = registry.get(catalog)
|
|
146
|
+
return res && res.ok ? res.snapshot : null
|
|
147
|
+
} catch (_) { return null }
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** 当前 workspace 的聚合列表(懒重建;corpus 变化或新证据后失效)。 */
|
|
151
|
+
function aggregatesFor(wsRef, corpusSnap) {
|
|
152
|
+
const miv = corpusSnap ? corpusSnap.memoryIndexVersion : null
|
|
153
|
+
if (aggCache && aggCache.wsRef === wsRef && aggCache.miv === miv) return aggCache.list
|
|
154
|
+
const st = storeFor()
|
|
155
|
+
const loaded = st.loadEvents()
|
|
156
|
+
const rebuilt = rebuildAggregates(loaded.events, corpusSnap ? corpusSnap.records : [])
|
|
157
|
+
const list = (wsRef && rebuilt.byWorkspaceRef.get(wsRef)) || []
|
|
158
|
+
aggCache = { wsRef, miv, list }
|
|
159
|
+
return list
|
|
160
|
+
}
|
|
161
|
+
function invalidateAggregates() { aggCache = null }
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* 拉长观察窗口(2026-08-27,working memory 实时定位):取最近 24 段(含 CoT/reasoning),
|
|
165
|
+
* 提取 CJK 关键词(2-4 gram 高频)+ 尾部原文,供 C2 检索/JS 判定。比旧版(8 段截尾)
|
|
166
|
+
* 覆盖面更广,让 CoT 里的 recall 意图能浮现。预算 ≤2000 字符。
|
|
167
|
+
*/
|
|
168
|
+
function buildObserveWindowText(runtime, seg) {
|
|
169
|
+
try {
|
|
170
|
+
const all = runtime.segments ? runtime.segments.snapshot() : []
|
|
171
|
+
const recent = all.slice(-24)
|
|
172
|
+
// 2026-08-27 query 分源(用户裁定):user 段用消息文本为主;reasoning/CoT 段用 CoT 文本为主。
|
|
173
|
+
// 两类段都参与判定(拟合高就激发),query 反映"当前在想什么/要什么"。
|
|
174
|
+
const userTexts = []
|
|
175
|
+
const cotTexts = []
|
|
176
|
+
for (const w of recent) {
|
|
177
|
+
const k = w && w.kind
|
|
178
|
+
const t = (w && w.text) || ''
|
|
179
|
+
if (!t) continue
|
|
180
|
+
if (k === 'user') userTexts.push(t)
|
|
181
|
+
else if (k === 'reasoning' || k === 'assistant') cotTexts.push(t)
|
|
182
|
+
}
|
|
183
|
+
const curText = (seg && seg.text) || ''
|
|
184
|
+
const segKind = seg && seg.kind
|
|
185
|
+
// 主 query:当前段类型决定——user 段用消息文本,CoT 段用 CoT 文本。
|
|
186
|
+
// 取拼接后尾部 1200(最近的思考/消息才代表"当下",旧版取头部偏早内容)。
|
|
187
|
+
// 兜底:窗口无对应类型时用当前段文本。
|
|
188
|
+
let main
|
|
189
|
+
if (segKind === 'reasoning') {
|
|
190
|
+
const tail = cotTexts.slice(-2).join(' ')
|
|
191
|
+
main = tail.length > 1200 ? tail.slice(-1200) : (tail || curText)
|
|
192
|
+
} else {
|
|
193
|
+
if (!userTexts.length && curText) userTexts.push(curText)
|
|
194
|
+
const tail = userTexts.slice(-2).join(' ')
|
|
195
|
+
main = tail.length > 1200 ? tail.slice(-1200) : tail
|
|
196
|
+
}
|
|
197
|
+
// CoT 回忆关键词(辅助信号,有回忆意图才提取)
|
|
198
|
+
const cotKws = extractRecallKeywords(cotTexts.join(' '))
|
|
199
|
+
const kws = cotKws.slice(0, 12)
|
|
200
|
+
return main + (kws.length ? ' 联想:' + kws.join(' ') : '')
|
|
201
|
+
} catch (_) { return ((seg && seg.text) || '') }
|
|
202
|
+
}
|
|
203
|
+
/** 提取 CoT 中"回忆意图"关键词(CJK 2-3 gram 高频)。
|
|
204
|
+
* 2026-08-27 Review 修正:回忆锚词用于判断"CoT 是否在回忆"(有锚词→提取全部高频词,
|
|
205
|
+
* 联想广度;无锚词→返回空,不强加无关联想)。不用锚词过滤 gram(会滤掉 DSH推理档位 这类主题词)。 */
|
|
206
|
+
function extractRecallKeywords(cotText) {
|
|
207
|
+
const s = String(cotText || '').replace(/\s+/g, '')
|
|
208
|
+
const sl = s.toLowerCase()
|
|
209
|
+
const RECALL_ANCHORS = ['之前', '上次', '当时', '记得', '查阅', '检索', '决策', '方案', '记录', '历史', '选择', '采用', '协议', '配置', '实现', '修复', '测试', '回忆', 'review', 'recall']
|
|
210
|
+
// 先判断是否在回忆:CoT 含任一锚词 → 是回忆场景(锚词小写化匹配,兼容 Review/Recall 写法)
|
|
211
|
+
const inRecall = RECALL_ANCHORS.some((a) => sl.includes(a))
|
|
212
|
+
if (!inRecall) return []
|
|
213
|
+
const grams = new Map()
|
|
214
|
+
for (let n = 2; n <= 3 && n <= s.length; n++) {
|
|
215
|
+
for (let i = 0; i + n <= s.length; i++) {
|
|
216
|
+
const g = s.slice(i, i + n)
|
|
217
|
+
if (/[\u4e00-\u9fff]/.test(g)) grams.set(g, (grams.get(g) || 0) + 1)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return [...grams.entries()].filter(([, c]) => c >= 2).sort((a, b) => b[1] - a[1]).slice(0, 16).map(([g]) => g)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* M2 Segment accept 后调用(index.js ingestEnvelope 内 fire-and-forget):
|
|
225
|
+
* user/assistant 文本先跑 cite/correction 扫描;随后组装并推送 envelope。
|
|
226
|
+
*/
|
|
227
|
+
function onSegmentAccepted(runtime, seg, envelope) {
|
|
228
|
+
try {
|
|
229
|
+
if (!effectiveEnabled()) { diagCtx('ctx-host skip: gates-off assoc=' + !!engine.config.associativeMemoryEnabled + ' bridge=' + !!engine.config.contextBridgeEnabled); return }
|
|
230
|
+
if (runtime.disposed) { diagCtx('ctx-host skip: runtime-disposed'); return }
|
|
231
|
+
const st = stateFor(runtime)
|
|
232
|
+
if (st.disposed) { diagCtx('ctx-host skip: st-disposed'); return }
|
|
233
|
+
const envPayload = (envelope && envelope.payload) || {}
|
|
234
|
+
// child/plugin 触发抑制(与 M4 同序;volatile 计数)
|
|
235
|
+
const isChild = !!(runtime.agent && runtime.agent.session && runtime.agent.session.header && runtime.agent.session.header.parentSession)
|
|
236
|
+
if (isChild && engine.config.contextBridgeObserveChildSessions !== true) {
|
|
237
|
+
pushDrop('child-session', seg.contextVersion); return
|
|
238
|
+
}
|
|
239
|
+
if (isChild) { diagCtx('ctx-host observe: child-session (observe-switch on) cv=' + seg.contextVersion) }
|
|
240
|
+
// M7.5 精确化(2026-08-26):仅拦截本插件自己的注入(auto-summary/welcome-back 等),
|
|
241
|
+
// 防止自动沉淀内容污染 trigger;harness 对续接会话打的来源标记不得误伤用户手动输入。
|
|
242
|
+
if (seg.kind === 'user' && /auto-memory/i.test(String(envPayload.sourcePlugin || ''))) {
|
|
243
|
+
pushDrop('plugin-generated-trigger', seg.contextVersion); return
|
|
244
|
+
}
|
|
245
|
+
const paths = pathsByKey.get(String(runtime.key || '')) || null
|
|
246
|
+
// M7-8 live-parity 诊断:记录最近 Segment 的 runtime key/sessionRef(最小投影)
|
|
247
|
+
lastSegmentRuntimeKey = String(runtime.key || '')
|
|
248
|
+
lastSegmentSessionRef = String(runtime.sessionId || '').slice(0, 24)
|
|
249
|
+
if (!paths) { pushDrop('no-paths-captured', seg.contextVersion); return }
|
|
250
|
+
const corpusSnap = loadCorpus(paths)
|
|
251
|
+
// ---- cite / correction(user+assistant 可见文本;precision-first,需完整 token+provenance)----
|
|
252
|
+
if ((seg.kind === 'user' || seg.kind === 'assistant') && seg.text && corpusSnap && corpusSnap.records.length) {
|
|
253
|
+
void emitTextEvidence({ seg, paths, corpusSnap })
|
|
254
|
+
}
|
|
255
|
+
// ---- M7-8 Host Index Sync Orchestration ----
|
|
256
|
+
// 时序修正(§19.10):index 必须先于 context_push 就绪——Python 在未收到全库语料前
|
|
257
|
+
// 无法做语义检索,若先 push context 会得到 index-not-ready 或空候选。
|
|
258
|
+
// 流程:ensureIndexReady(幂等缓存)→ ready 后才推最新 frame;旧 frame 在新 contextVersion
|
|
259
|
+
// 到达时被 cancelStale 作废。同步失败 → 结构化记录,不阻塞本 Segment 的 envelope 组装,
|
|
260
|
+
// 但 context_push 仅在 ready 后发送(失败则不发送,待下一 Segment 重试)。
|
|
261
|
+
// M7-8:python sink 才需 index-ready 门(fake/null sink 不依赖 python corpus)
|
|
262
|
+
const needIndexReady = sinkMode() === 'python'
|
|
263
|
+
const readyPromise = needIndexReady ? ensureIndexReadyFor(paths, corpusSnap) : Promise.resolve({ ready: true })
|
|
264
|
+
// ---- envelope 组装与推送 ----
|
|
265
|
+
const ringItems = runtime.segments ? runtime.segments.snapshot() : []
|
|
266
|
+
const winRaw = ringItems.slice(-CONTEXT_BRIDGE_BUDGET_V1.maxSegments).map((w) => ({
|
|
267
|
+
segmentId: w.id, digest: w.digest, kind: w.kind, eventSeq: w.eventSeq,
|
|
268
|
+
contextVersion: w.contextVersion, ts: w.ts, text: w.text,
|
|
269
|
+
toolName: w.toolName != null ? w.toolName : null,
|
|
270
|
+
toolOk: w.toolOk != null ? w.toolOk : null,
|
|
271
|
+
}))
|
|
272
|
+
const wsRef = workspaceRefOf(paths.workspaceKey)
|
|
273
|
+
const qpInput = {
|
|
274
|
+
trigger: { segmentId: seg.id, segmentDigest: seg.digest, kind: seg.kind, eventType: seg.eventType || 'session/event', ts: seg.ts },
|
|
275
|
+
window: winRaw,
|
|
276
|
+
}
|
|
277
|
+
const qp = buildQueryPlan(qpInput)
|
|
278
|
+
const ls = corpusSnap
|
|
279
|
+
? lexicalSearch(corpusSnap, qp, { triggerTs: seg.ts, mode: 'prefetch', dayBoundaryMinutes: Number(engine.config.dayBoundaryMinutes) || 450 })
|
|
280
|
+
: { kept: [] }
|
|
281
|
+
// C2 内置语义臂(2026-08-26):排名需 embed,挂进与 push 同一条延迟链;
|
|
282
|
+
// envelope 构建为确定性纯函数,后移到 ready/rank 就绪后执行是安全的。
|
|
283
|
+
// 钩子由 index.js 注入(engine._jsSemanticRank);缺失/失败 → 词法序原样。
|
|
284
|
+
const c2RankPromise = (typeof engine._jsSemanticRank === 'function' && seg.text && corpusSnap && Array.isArray(corpusSnap.records) && corpusSnap.records.length)
|
|
285
|
+
? Promise.resolve().then(() => engine._jsSemanticRank(corpusSnap, buildObserveWindowText(runtime, seg))).catch(() => null)
|
|
286
|
+
: Promise.resolve(null)
|
|
287
|
+
void Promise.all([readyPromise, c2RankPromise]).then(([readyRes, rank]) => {
|
|
288
|
+
if (!(readyRes && readyRes.ready)) {
|
|
289
|
+
stats.pushesRejected++
|
|
290
|
+
pushDrop('index-not-ready:' + ((readyRes && readyRes.reason) || 'unknown'), runtime.contextVersion)
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
if (st.disposed || runtime.disposed) return
|
|
294
|
+
let keptList = ls.kept
|
|
295
|
+
if (rank && rank.scores && rank.scores.size) {
|
|
296
|
+
// D6 融合候选池=词法 kept ∪ 稠密 top-K(语料中找回记录对象;语义强命中但词法零分者由此入选)
|
|
297
|
+
const poolMap = new Map()
|
|
298
|
+
for (const k of ls.kept) if (k && k.memoryId) poolMap.set(k.memoryId, k)
|
|
299
|
+
const denseTop = [...rank.scores.entries()].sort((a, b) => b[1] - a[1]).slice(0, CONTEXT_BRIDGE_BUDGET_V1.maxMemoryRefs)
|
|
300
|
+
for (const [mid] of denseTop) {
|
|
301
|
+
if (!poolMap.has(mid)) {
|
|
302
|
+
const rec = (corpusSnap.records || []).find((r) => r && r.memoryId === mid)
|
|
303
|
+
if (rec) poolMap.set(mid, rec)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
keptList = fuseD6Pre([...poolMap.values()].map((k) => ({
|
|
307
|
+
memoryId: k.memoryId,
|
|
308
|
+
lex: (k.scores && Number(k.scores.total)) || 0,
|
|
309
|
+
dense: typeof rank.scores.get(k.memoryId) === 'number' ? rank.scores.get(k.memoryId) : null,
|
|
310
|
+
}))).map((f) => poolMap.get(f.memoryId)).filter(Boolean)
|
|
311
|
+
}
|
|
312
|
+
const refs = []
|
|
313
|
+
for (const k of keptList.slice(0, CONTEXT_BRIDGE_BUDGET_V1.maxMemoryRefs)) {
|
|
314
|
+
const r = buildAuthorizedMemoryRefFromRecord(k, k.text)
|
|
315
|
+
if (r.ok) refs.push(r.ref)
|
|
316
|
+
}
|
|
317
|
+
const aggs = aggregatesFor(wsRef, corpusSnap).slice(0, CONTEXT_BRIDGE_BUDGET_V1.maxEvidenceItems)
|
|
318
|
+
const built = buildContextPushEnvelopePre({
|
|
319
|
+
session: { sessionId: runtime.sessionId || '', agentId: runtime.agentId || '', workspaceKey: paths.workspaceKey, scope: 'Workspace' },
|
|
320
|
+
cursor: { eventSeq: seg.eventSeq, nativeSeq: seg.nativeSeq, contextVersion: runtime.contextVersion },
|
|
321
|
+
index: { memoryIndexVersion: corpusSnap ? corpusSnap.memoryIndexVersion : ('idx_' + '0'.repeat(32)), sourceEpochs: corpusSnap ? corpusSnap.sources.map((s) => s.sourceEpoch) : [] },
|
|
322
|
+
trigger: {
|
|
323
|
+
segmentId: seg.id, digest: seg.digest, kind: seg.kind, eventSeq: seg.eventSeq,
|
|
324
|
+
contextVersion: seg.contextVersion, ts: seg.ts, text: seg.text,
|
|
325
|
+
},
|
|
326
|
+
window: winRaw.slice(0, CONTEXT_BRIDGE_BUDGET_V1.maxSegments),
|
|
327
|
+
memoryRefs: refs,
|
|
328
|
+
evidence: aggs,
|
|
329
|
+
now: Date.now(),
|
|
330
|
+
policyVersionGate: GATE_POLICY_VERSION,
|
|
331
|
+
policyVersionLexical: LEXICAL_POLICY_VERSION,
|
|
332
|
+
})
|
|
333
|
+
if (!built.ok) { pushDrop('envelope:' + built.reason, runtime.contextVersion); return }
|
|
334
|
+
stats.envelopesBuilt++
|
|
335
|
+
lastFrameIdentity = {
|
|
336
|
+
observationId: built.frame.observationId,
|
|
337
|
+
contextVersion: built.frame.cursor.contextVersion,
|
|
338
|
+
windowCount: built.frame.window.length,
|
|
339
|
+
memoryRefCount: built.frame.memoryRefs.length,
|
|
340
|
+
evidenceCount: built.frame.evidence.length,
|
|
341
|
+
}
|
|
342
|
+
bridgeFor().cancelStale(built.frame.session.sessionId, built.frame.cursor.contextVersion)
|
|
343
|
+
void bridgeFor().push(built.frame).then((ack) => {
|
|
344
|
+
if (ack && ack.accepted) stats.pushesAccepted++
|
|
345
|
+
else stats.pushesRejected++
|
|
346
|
+
}).catch(() => { stats.errors++ })
|
|
347
|
+
st.lastPushedContextVersion = built.frame.cursor.contextVersion
|
|
348
|
+
// 2026-08-27 JS 判定闭环:C2 排名就绪时,用 JS 判定核(fv2 策略工件)做决策;
|
|
349
|
+
// emit → 经 M6 activationHost 投递(Reference Tail)。完全独立于 Python。
|
|
350
|
+
// 判定侧冷却(jsDecideCooldownRounds):emit 注入后 N 轮内不再判定,防连续唤起浪费 token。
|
|
351
|
+
// 2026-08-27 修正(用户裁定):user + reasoning(CoT)段都触发判定——滑动监测的初衷是
|
|
352
|
+
// 监听模型思维链,拟合度高就激发(不只用户消息,也不只困难场景)。query 分源:
|
|
353
|
+
// user 段用消息文本,CoT 段用 CoT 文本。assistant(可见输出)同样触发——非推理模型
|
|
354
|
+
// 无 reasoning 段时它是唯一"模型在说什么"的信号;复述风险由 echo veto+冷却+阈值三层兜底。
|
|
355
|
+
if ((seg.kind === 'user' || seg.kind === 'tool' || seg.kind === 'reasoning' || seg.kind === 'assistant') && rank && rank.scores && rank.scores.size && typeof engine._jsDecide === 'function' && typeof engine._activationHost !== 'undefined' && engine._activationHost) {
|
|
356
|
+
try {
|
|
357
|
+
// 2026-08-27 判定观测:按段类型计数(真实数据验证 CoT 触发)
|
|
358
|
+
stats.jsDecideRuns++
|
|
359
|
+
stats.jsDecideByKind[seg.kind] = (stats.jsDecideByKind[seg.kind] || 0) + 1
|
|
360
|
+
const cd = Math.max(0, Number(engine.config.jsDecideCooldownRounds) || 5)
|
|
361
|
+
const stNow = stateFor(runtime)
|
|
362
|
+
if (cd > 0 && stNow._jsDecideNextAt && Date.now() < stNow._jsDecideNextAt) return
|
|
363
|
+
var jsDecideQueryText = buildObserveWindowText(runtime, seg)
|
|
364
|
+
void Promise.resolve()
|
|
365
|
+
.then(() => engine._jsDecide(jsDecideQueryText, {
|
|
366
|
+
scores: rank.scores,
|
|
367
|
+
_records: corpusSnap ? corpusSnap.records : [],
|
|
368
|
+
// 2026-08-28 孪生对齐:词法分喂给 fv2 候选融合(Python 口径=稠密top8按D6融合序)
|
|
369
|
+
_lex: (function () { const m = new Map(); for (const k of ls.kept) if (k && k.memoryId) m.set(k.memoryId, (k.scores && Number(k.scores.total)) || 0); return m })(),
|
|
370
|
+
}, built.frame))
|
|
371
|
+
.then(async (dec) => {
|
|
372
|
+
// 2026-08-27 判定 shadow 日志:每条决策落盘(含 suppress/prefetch/emit),
|
|
373
|
+
// 供真实数据验证 CoT 触发是否工作。fail closed:日志失败不影响判定链。
|
|
374
|
+
try {
|
|
375
|
+
if (dec && dec.ok) {
|
|
376
|
+
jsDecideShadowLog({
|
|
377
|
+
t: Date.now(), kind: seg.kind,
|
|
378
|
+
decision: dec.decision, lane: dec.lane || '',
|
|
379
|
+
reasonCodes: (dec.reasonCodes || []).slice(0, 6),
|
|
380
|
+
intentProb: dec.features ? Math.round(dec.features.intentProb * 1e4) / 1e4 : null,
|
|
381
|
+
dialogAct: dec.features ? dec.features.dialogueAct : null,
|
|
382
|
+
// 2026-08-28 对齐后特征:fv2 候选=稠密top8融合序;margin=融合1/2名稠密分差
|
|
383
|
+
candN: dec._candN != null ? dec._candN : (rank.scores ? rank.scores.size : 0),
|
|
384
|
+
denseTop: dec._denseTop != null ? Math.round(dec._denseTop * 1e4) / 1e4 : null,
|
|
385
|
+
margin: dec._margin != null ? Math.round(dec._margin * 1e4) / 1e4 : null,
|
|
386
|
+
hit: !!dec._hit,
|
|
387
|
+
emitMode: (typeof engine.jsEmitMode === 'function') ? (() => { try { return engine.jsEmitMode() } catch (_) { return 'shadow' } })() : 'shadow',
|
|
388
|
+
sessionRef: String(runtime.sessionId || '').slice(0, 12),
|
|
389
|
+
wsRef: (function () { try { return workspaceRefOf((pathsByKey.get(String(runtime.key || '')) || {}).workspaceKey || '') } catch (_) { return '' } })(),
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
} catch (_) {}
|
|
393
|
+
if (!dec || !dec.ok || dec.decision !== 'emit') return
|
|
394
|
+
stats.jsDecideEmits++
|
|
395
|
+
// 2026-08-27 JS 发射门:读 activationEmitMode(与 Python 同源)。
|
|
396
|
+
// shadow=只记录不注入(canary-explicit/active 才注入)。
|
|
397
|
+
try { if (typeof engine.jsEmitMode === 'function' && engine.jsEmitMode() === 'shadow') { stats.jsDecideShadowed++; return } } catch (_) {}
|
|
398
|
+
// emit → 冷却(时间窗,默认 60s 内不再判定,防止连续注入)
|
|
399
|
+
if (cd > 0) stNow._jsDecideNextAt = Date.now() + cd * 60000
|
|
400
|
+
// 构建 ActivationRequestPre 帧(与 Python _build_activation 同形状),走 M6 注入
|
|
401
|
+
const session = built.frame.session || {}
|
|
402
|
+
const cursor = built.frame.cursor || {}
|
|
403
|
+
const obs = built.frame.observationId || ''
|
|
404
|
+
const act = {
|
|
405
|
+
schemaVersion: 1, namespace: 'dsh-auto-memory', kind: 'activation_request',
|
|
406
|
+
activationId: 'act_' + (obs.startsWith('obs_') ? obs.slice(8, 40) : 'jsdecide'),
|
|
407
|
+
observationId: obs, workerEpoch: 'js-decide-pre-v1',
|
|
408
|
+
sessionId: session.sessionId || '', agentId: session.agentId || '',
|
|
409
|
+
workspaceKey: session.workspaceKey || '', scope: session.scope || 'Workspace',
|
|
410
|
+
contextVersion: cursor.contextVersion || 0, memoryIndexVersion: built.frame.index ? built.frame.index.memoryIndexVersion : '',
|
|
411
|
+
threshold: { policyVersion: dec.features ? 'activation_policy_v2' : 'activation_policy_v2', score: (dec.features && dec.features.intentProb) || 0, threshold: 0.45, reason: ('js-decide lane=' + dec.lane + ' ' + (dec.reasonCodes || []).join(',')) },
|
|
412
|
+
level: 'excerpt',
|
|
413
|
+
candidates: (function () {
|
|
414
|
+
// 2026-08-27 优化③:候选方案档位(balanced=3×40 / dense=6×20 / custom=自定义)
|
|
415
|
+
var scheme = String(engine.config.jsDecideCandidateScheme || 'balanced')
|
|
416
|
+
var n, ex
|
|
417
|
+
if (scheme === 'dense') { n = 6; ex = 20 }
|
|
418
|
+
else if (scheme === 'custom') { n = Math.max(1, Math.min(8, Number(engine.config.jsDecideCandidatesN) || 4)); ex = Math.max(20, Math.min(480, Number(engine.config.jsDecideExcerptChars) || 40)) }
|
|
419
|
+
else { n = 3; ex = 40 } // balanced 默认
|
|
420
|
+
return refs.slice(0, n).map(function (r, i) {
|
|
421
|
+
return {
|
|
422
|
+
candidateId: 'cand_' + String(obs || 'js').slice(-32) + i,
|
|
423
|
+
memoryId: r.memoryId, anchorId: r.anchorId || '', scope: r.scope || 'Workspace',
|
|
424
|
+
sourceRef: r.sourceRef || '', sourceEpoch: r.sourceEpoch || '', sourceVersion: r.sourceVersion || 1,
|
|
425
|
+
fileDigest: r.fileDigest || '', recordDigest: r.recordDigest || '',
|
|
426
|
+
score: rank.scores.get(r.memoryId) || 0, excerpt: (r.excerpt || '').slice(0, ex),
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
})(),
|
|
430
|
+
ttlSteps: 3, createdAt: Date.now(), expiresAt: Date.now() + 180000,
|
|
431
|
+
}
|
|
432
|
+
// M8 技能召回(Memory Hub):emit 命中时,若记忆中枢有与当前 query 相关的 active skill,
|
|
433
|
+
// 把 checklist 附加到注入内容(推荐候选),让 AI 按固定流程执行。纯增强,失败静默。
|
|
434
|
+
// 2026-08-28 P1⑥:优先 C2 稠密匹配(技能标题+步骤进嵌入索引,miv=技能集指纹,
|
|
435
|
+
// 集合不变则缓存命中);不可用回退词法 2-gram。阈值 0.6(e5 相关内容带)。单 offer 出口。
|
|
436
|
+
try {
|
|
437
|
+
const hub = engine._memoryHub
|
|
438
|
+
const skillEnabled = engine.config.memoryHubEnabled === true && engine.config.procedurePromotionEnabled !== false
|
|
439
|
+
if (hub && skillEnabled && hub.stores && hub.stores.procedures) {
|
|
440
|
+
const actives = hub.stores.procedures.activeProcedures()
|
|
441
|
+
let hit = null
|
|
442
|
+
if (actives.length && typeof engine._jsSemanticRank === 'function') {
|
|
443
|
+
try {
|
|
444
|
+
const recs = actives.map((p) => ({ memoryId: String(p.procedureId), text: String(p.title || '') + ' ' + (Array.isArray(p.steps) ? p.steps.join(' ') : String(p.steps || '')) }))
|
|
445
|
+
const miv = 'idx_' + createHash('sha256').update(recs.map((r) => r.memoryId).join(',')).digest('hex').slice(0, 32)
|
|
446
|
+
const rank = await engine._jsSemanticRank({ memoryIndexVersion: miv, records: recs }, jsDecideQueryText)
|
|
447
|
+
if (rank && rank.scores && rank.scores.size) {
|
|
448
|
+
const [topId, topScore] = [...rank.scores.entries()].sort((a, b) => b[1] - a[1])[0]
|
|
449
|
+
if (topScore >= 0.6) hit = actives.find((p) => String(p.procedureId) === topId) || null
|
|
450
|
+
}
|
|
451
|
+
} catch (_) {}
|
|
452
|
+
}
|
|
453
|
+
if (!hit) {
|
|
454
|
+
const ql = String(jsDecideQueryText || '').toLowerCase()
|
|
455
|
+
hit = actives.find((p) => {
|
|
456
|
+
if (!p || !p.title) return false
|
|
457
|
+
const t = String(p.title).toLowerCase()
|
|
458
|
+
if (ql.length >= 2 && t && ql.includes(t.slice(0, 2))) return true
|
|
459
|
+
if (t && ql.includes(t)) return true
|
|
460
|
+
return false
|
|
461
|
+
}) || null
|
|
462
|
+
}
|
|
463
|
+
if (hit) {
|
|
464
|
+
const cl = hub.stores.procedures.renderChecklist(hit.procedureId)
|
|
465
|
+
if (cl && cl.text) {
|
|
466
|
+
// level 一并由 renderChecklist 给出(高风险自动降级 hint),供 M6 技能段渲染
|
|
467
|
+
act.skill = { procedureId: hit.procedureId, title: hit.title, level: cl.level || 'checklist', text: cl.text.slice(0, 1200) }
|
|
468
|
+
try { hub.stores.procedures.touch(hit.procedureId) } catch (_) {} // Hermes:last_used 时钟
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
} catch (_) {}
|
|
473
|
+
engine._activationHost.offerExternalActivation(act)
|
|
474
|
+
}).catch(() => {})
|
|
475
|
+
} catch (_) {}
|
|
476
|
+
}
|
|
477
|
+
}).catch(() => { stats.pushesRejected++ })
|
|
478
|
+
} catch (e) { stats.errors++ }
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/** M7-8:index ready 编排;corpus 缺失/miv 无效→{ok:false,ready:false}。 */
|
|
482
|
+
async function ensureIndexReadyFor(paths, corpusSnap) {
|
|
483
|
+
if (!engine._indexSyncHost || !corpusSnap) return { ok: false, ready: false, reason: 'no-corpus' }
|
|
484
|
+
try {
|
|
485
|
+
return await engine._indexSyncHost.ensureIndexReady(corpusSnap, paths, 'Workspace', { runtimeKey: '(ctx-host)' })
|
|
486
|
+
} catch (e) { return { ok: false, ready: false, reason: 'orchestrator-error' } }
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/** cite/correction 落盘(异步链;幂等由 evidenceId 保证)。 */
|
|
490
|
+
async function emitTextEvidence(ctxT) {
|
|
491
|
+
const { seg, paths, corpusSnap } = ctxT
|
|
492
|
+
const coords = {
|
|
493
|
+
sessionId: seg.sessionId || '', eventSeq: seg.eventSeq, nativeSeq: seg.nativeSeq,
|
|
494
|
+
contextVersion: seg.contextVersion, workspaceKey: paths.workspaceKey, ts: seg.ts,
|
|
495
|
+
}
|
|
496
|
+
const cites = createCiteEvidencesFromText({ text: seg.text, knownRecords: corpusSnap.records, coords })
|
|
497
|
+
const corrections = seg.kind === 'user'
|
|
498
|
+
? createCorrectionEvidencesFromText({ text: seg.text, knownRecords: corpusSnap.records, coords })
|
|
499
|
+
: []
|
|
500
|
+
await persistEvidence([...cites, ...corrections])
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* frozen tools/result 观察(read coverage):
|
|
505
|
+
* precision-first v1——仅当 ok=true 且 resultPreview 含完整 memoryId token 且该 token 在当前授权 corpus 中,
|
|
506
|
+
* 且记录切片被 preview 归一化包含(coverage>0)时建 read evidence;fileDigest 以 corpus 快照为 observed 值做 stale 门。
|
|
507
|
+
*/
|
|
508
|
+
function onToolResult(runtime, envelope) {
|
|
509
|
+
try {
|
|
510
|
+
if (!effectiveEnabled()) return
|
|
511
|
+
if (!runtime || !envelope || runtime.disposed) return
|
|
512
|
+
const payload = envelope.payload || {}
|
|
513
|
+
if (process.env.DSH_CTX_DEBUG) console.error('[ctx-host-diag] toolResult ok=' + payload.ok + ' name=' + payload.name + ' previewLen=' + String(payload.resultPreview || '').length)
|
|
514
|
+
if (payload.ok !== true) return
|
|
515
|
+
const preview = typeof payload.resultPreview === 'string' ? payload.resultPreview : ''
|
|
516
|
+
if (!preview || preview.indexOf('mem_') === -1) return
|
|
517
|
+
const paths = pathsByKey.get(String(runtime.key || '')) || null
|
|
518
|
+
if (!paths) return
|
|
519
|
+
const corpusSnap = loadCorpus(paths)
|
|
520
|
+
if (!corpusSnap || !corpusSnap.records.length) return
|
|
521
|
+
const ids = [...new Set(preview.match(/mem_[0-9a-f]{32}/g) || [])]
|
|
522
|
+
if (!ids.length) return
|
|
523
|
+
const candidates = corpusSnap.records.filter((r) => ids.includes(r.memoryId))
|
|
524
|
+
if (process.env.DSH_CTX_DEBUG) console.error('[ctx-host-diag] ids=' + ids.join(',') + ' corpusRecords=' + corpusSnap.records.length + ' candidates=' + candidates.length)
|
|
525
|
+
if (!candidates.length) return
|
|
526
|
+
const observedDigest = pickObservedFileDigest(corpusSnap, candidates[0].sourceRef)
|
|
527
|
+
const cov = computeReadCoverage(candidates, { text: preview, observedFileDigest: observedDigest })
|
|
528
|
+
if (process.env.DSH_CTX_DEBUG) console.error('[ctx-host-diag] covered=' + cov.covered.length + ' stale=' + cov.stale.length + ' covVals=' + JSON.stringify(cov.covered.map((c) => c.coverage)))
|
|
529
|
+
stats.stalesSeen += cov.stale.length
|
|
530
|
+
const coordsBase = {
|
|
531
|
+
sessionId: envelope.sessionId || '', eventSeq: envelope.eventSeq, nativeSeq: envelope.nativeSeq,
|
|
532
|
+
contextVersion: envelope.callId ? runtime.contextVersion : runtime.contextVersion,
|
|
533
|
+
callId: envelope.callId || undefined, workspaceKey: paths.workspaceKey, ts: envelope.timestamp || Date.now(),
|
|
534
|
+
}
|
|
535
|
+
const evidences = []
|
|
536
|
+
for (const c of cov.covered) {
|
|
537
|
+
const r = c.record
|
|
538
|
+
const ev = createAccessEvidencePre({
|
|
539
|
+
...coordsBase, kind: 'read', memoryId: r.memoryId, anchorId: r.anchorId, scope: r.scope,
|
|
540
|
+
sourceRef: r.sourceRef, sourceEpoch: r.sourceEpoch, sourceVersion: r.sourceVersion,
|
|
541
|
+
fileDigest: r.fileDigest, recordDigest: r.recordDigest, coverage: c.coverage,
|
|
542
|
+
})
|
|
543
|
+
if (ev.ok) evidences.push(ev.evidence)
|
|
544
|
+
}
|
|
545
|
+
stats.readsCovered += evidences.length
|
|
546
|
+
if (evidences.length) void persistEvidence(evidences)
|
|
547
|
+
} catch (e) { stats.errors++ }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/** 从 corpus sources 里取 sourceRef 对应文件的当前 fileDigest(stale 门观测值)。 */
|
|
551
|
+
function pickObservedFileDigest(corpusSnap, sourceRef) {
|
|
552
|
+
const src = (corpusSnap.sources || []).find((s) => s.sourceRef === sourceRef)
|
|
553
|
+
return src ? src.fileDigest : undefined
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
async function persistEvidence(list) {
|
|
557
|
+
if (!list.length) return
|
|
558
|
+
const st = storeFor()
|
|
559
|
+
for (const ev of list) {
|
|
560
|
+
const r = await st.append(ev)
|
|
561
|
+
if (r.ok) stats.evidenceAppended++
|
|
562
|
+
else if (r.reason === 'duplicate-evidence') stats.evidenceDuplicates++
|
|
563
|
+
else stats.evidenceFailed++
|
|
564
|
+
}
|
|
565
|
+
invalidateAggregates()
|
|
566
|
+
// 2026-08-28 M9 evidence 直达分流(Hermes 借鉴):证据按 memoryId 关联到
|
|
567
|
+
// sourceMemoryIds 包含它的技能→addEvidence(touch lastUsedAt,驱动老化时钟)。
|
|
568
|
+
// memoryHubEnabled 门;逐条 fail closed;无映射则跳过。诊断经 diagCtx。
|
|
569
|
+
try {
|
|
570
|
+
const hub = engine._memoryHub
|
|
571
|
+
if (engine.config.memoryHubEnabled !== true) diagCtx('hub evidence feed skip: hub-off')
|
|
572
|
+
else if (!hub || !hub.stores || !hub.stores.procedures) diagCtx('hub evidence feed skip: no-hub')
|
|
573
|
+
else {
|
|
574
|
+
// sessionRef:证据事件自带 sessionId 优先;cite 路径的 seg 无 sessionId 时
|
|
575
|
+
// 用 lastSegmentSessionRef(会话多样性计数依赖它,空会话 Ref 不计会话)
|
|
576
|
+
const sessionRef = String((list[0] && list[0].sessionId) || lastSegmentSessionRef || '').slice(0, 48)
|
|
577
|
+
const all = hub.stores.procedures.query()
|
|
578
|
+
for (const ev of list) {
|
|
579
|
+
if (!ev || !ev.memoryId) continue
|
|
580
|
+
let matched = 0
|
|
581
|
+
for (const p of all) {
|
|
582
|
+
if (!Array.isArray(p.sourceMemoryIds) || !p.sourceMemoryIds.includes(ev.memoryId)) continue
|
|
583
|
+
matched++
|
|
584
|
+
if (['seen', 'read', 'cite', 'reuse', 'success', 'correction'].includes(ev.kind)) {
|
|
585
|
+
const ar = hub.stores.procedures.addEvidence(p.procedureId, { kind: ev.kind, sessionRef })
|
|
586
|
+
diagCtx('hub evidence feed: ' + ev.kind + ' → ' + String(p.procedureId).slice(0, 18) + ' ok=' + !!(ar && ar.ok) + ' counts=' + JSON.stringify(ar && ar.evidence))
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
if (!matched) diagCtx('hub evidence feed: no-proc-match ' + ev.kind + ' ' + String(ev.memoryId).slice(0, 18) + ' (procs=' + all.length + ')')
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
} catch (e) { diagCtx('hub evidence feed error: ' + String(e && e.message || e).slice(0, 100)) }
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** JS 判定 shadow 日志(2026-08-27,真实数据验证):append-only JSONL,与 Python
|
|
596
|
+
* activation-shadow-v2.jsonl 同目录对齐。无原文/无绝对路径,身份最小投影。
|
|
597
|
+
* fail closed:任何异常静默,不阻断判定链。 */
|
|
598
|
+
const JSDECIDE_SHADOW_MAX_LINES = 256
|
|
599
|
+
function jsDecideShadowLog(entry) {
|
|
600
|
+
try {
|
|
601
|
+
const dir = path.join(dshHome(), 'memory', 'semantic')
|
|
602
|
+
const f = path.join(dir, 'js-decide-shadow.jsonl')
|
|
603
|
+
let line = JSON.stringify(entry)
|
|
604
|
+
if (!line) return
|
|
605
|
+
appendFileSync(f, line + '\n', 'utf8')
|
|
606
|
+
// 有界:超过上限时保留最后 N 行(读整文件→截尾→重写)
|
|
607
|
+
try {
|
|
608
|
+
const raw = readFileSync(f, 'utf8')
|
|
609
|
+
const lines = String(raw).split('\n').filter((l) => l.trim())
|
|
610
|
+
if (lines.length > JSDECIDE_SHADOW_MAX_LINES) {
|
|
611
|
+
writeFileSync(f, lines.slice(-JSDECIDE_SHADOW_MAX_LINES).join('\n') + '\n', 'utf8')
|
|
612
|
+
}
|
|
613
|
+
} catch (_) {}
|
|
614
|
+
} catch (_) {}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/** §17 式最小 debug 投影;关闭时严格 {enabled:false}。 */
|
|
618
|
+
function debugView() {
|
|
619
|
+
if (!effectiveEnabled()) return { enabled: false }
|
|
620
|
+
const b = bridgeFor()
|
|
621
|
+
return {
|
|
622
|
+
enabled: true,
|
|
623
|
+
contextPolicyVersion: CONTEXT_BRIDGE_POLICY_VERSION,
|
|
624
|
+
evidencePolicyVersion: EVIDENCE_POLICY_VERSION,
|
|
625
|
+
sinkKind: b.sinkKind(),
|
|
626
|
+
lastFrame: lastFrameIdentity,
|
|
627
|
+
// M7-8 live-parity 诊断(最小投影,不泄路径/文本):capture 键、最近 Segment 身份、最近 drop
|
|
628
|
+
capturedPathKeys: [...pathsByKey.keys()].slice(0, 8),
|
|
629
|
+
lastSegmentRuntimeKey: lastSegmentRuntimeKey,
|
|
630
|
+
lastSegmentSessionRef: lastSegmentSessionRef,
|
|
631
|
+
evidenceDirPath: path.join(dshHome(), 'memory', 'evidence'),
|
|
632
|
+
durableEventsOnDisk: evidenceRootExists() ? storeFor().loadEvents().events.length : 0,
|
|
633
|
+
stats: { ...stats, bridge: { ...b.stats } },
|
|
634
|
+
storeStats: store ? { ...store.stats } : null,
|
|
635
|
+
recentDrops: volatileDrops.slice(-4),
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function collectStates() {
|
|
640
|
+
const out = []
|
|
641
|
+
try {
|
|
642
|
+
for (const rt of engine.runtimes.values()) {
|
|
643
|
+
const st = states.get(rt)
|
|
644
|
+
if (st) out.push({ runtime: rt, state: st })
|
|
645
|
+
}
|
|
646
|
+
} catch (_) {}
|
|
647
|
+
return out
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function disposeRuntime(runtime) {
|
|
651
|
+
const st = states.get(runtime)
|
|
652
|
+
if (st) st.disposed = true
|
|
653
|
+
states.delete(runtime)
|
|
654
|
+
}
|
|
655
|
+
function disposeAll(reason) {
|
|
656
|
+
for (const pair of collectStates()) disposeRuntime(pair.runtime)
|
|
657
|
+
volatileDrops.length = 0
|
|
658
|
+
if (bridge) void bridge.dispose(reason)
|
|
659
|
+
if (store) store.dispose(reason)
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
init() {},
|
|
664
|
+
capturePaths,
|
|
665
|
+
effectiveEnabled,
|
|
666
|
+
isLive: effectiveEnabled,
|
|
667
|
+
onSegmentAccepted,
|
|
668
|
+
onToolResult,
|
|
669
|
+
debugView,
|
|
670
|
+
getStats: () => ({ ...stats }),
|
|
671
|
+
disposeRuntime,
|
|
672
|
+
disposeAll,
|
|
673
|
+
// M6-3 接线点:按 memoryId+recordDigest 查当前 corpus 完整 provenance(seen evidence 需要 sourceEpoch/fileDigest)
|
|
674
|
+
findProvenance(workspaceKey, memoryId, recordDigest) {
|
|
675
|
+
const wsRef = workspaceRefOf(workspaceKey)
|
|
676
|
+
const paths = null
|
|
677
|
+
void paths
|
|
678
|
+
for (const [, p] of pathsByKey) {
|
|
679
|
+
if (workspaceRefOf(p.workspaceKey) !== wsRef) continue
|
|
680
|
+
const snap = loadCorpus(p)
|
|
681
|
+
if (!snap) continue
|
|
682
|
+
const rec = snap.records.find((r) => r.memoryId === memoryId && r.recordDigest === recordDigest)
|
|
683
|
+
if (rec) return { memoryId: rec.memoryId, anchorId: rec.anchorId, scope: rec.scope, sourceRef: rec.sourceRef, sourceEpoch: rec.sourceEpoch, sourceVersion: rec.sourceVersion, fileDigest: rec.fileDigest, recordDigest: rec.recordDigest }
|
|
684
|
+
}
|
|
685
|
+
return null
|
|
686
|
+
},
|
|
687
|
+
// M6-3 接线点:seen evidence 经同一隐私投影 store 落盘
|
|
688
|
+
appendEvidence(evList) { return persistEvidence(Array.isArray(evList) ? evList : [evList]) },
|
|
689
|
+
// 2026-08-30 M9:recent read/cite 证据对象(含完整 provenance)——供 consolidateTurn
|
|
690
|
+
// 创建 success evidence 驱动技能晋升(createSuccessEvidencePre 全仓零调用方的修复)。
|
|
691
|
+
recentEvidenceForSuccess(windowMs) {
|
|
692
|
+
try {
|
|
693
|
+
if (!effectiveEnabled()) return []
|
|
694
|
+
const st = storeFor()
|
|
695
|
+
const events = st.loadEvents().events
|
|
696
|
+
const cutoff = Date.now() - (Number(windowMs) || 300000)
|
|
697
|
+
const out = new Map()
|
|
698
|
+
for (const e of events) {
|
|
699
|
+
const ets = e.ts || e.createdAt || 0
|
|
700
|
+
if (ets < cutoff) continue
|
|
701
|
+
if (e.kind !== 'read' && e.kind !== 'cite') continue
|
|
702
|
+
if (!e.memoryId) continue
|
|
703
|
+
if (!out.has(e.memoryId)) out.set(e.memoryId, e)
|
|
704
|
+
}
|
|
705
|
+
return [...out.values()]
|
|
706
|
+
} catch (_) { return [] }
|
|
707
|
+
},
|
|
708
|
+
_volatileDrops: volatileDrops,
|
|
709
|
+
_statsForTest: stats,
|
|
710
|
+
_invalidateAggregatesForTest: invalidateAggregates,
|
|
711
|
+
}
|
|
712
|
+
}
|