@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/cordis.patch.yml CHANGED
@@ -1,9 +1,9 @@
1
- # dsh-auto-memory bundle patch: inserts the dual-face plugin row into the web
2
- # profile roster. Applied as a profile bundle layer (the `dsh.bundle.patch`
3
- # manifest field). The node half (exports ".") runs in the host process
4
- # (memory engine, /api/dsh-auto-memory routes, agent tools, prompt injection);
5
- # the `dsh.client` declaration in package.json makes the browser half
6
- # (exports "./client", served at /plugins/<id>/client.js) load in the web GUI.
7
- - insert:
8
- - id: auto-memory
9
- name: '@a9i5k4/dsh-auto-memory'
1
+ # dsh-auto-memory bundle patch: inserts the dual-face plugin row into the web
2
+ # profile roster. Applied as a profile bundle layer (the `dsh.bundle.patch`
3
+ # manifest field). The node half (exports ".") runs in the host process
4
+ # (memory engine, /api/dsh-auto-memory routes, agent tools, prompt injection);
5
+ # the `dsh.client` declaration in package.json makes the browser half
6
+ # (exports "./client", served at /plugins/<id>/client.js) load in the web GUI.
7
+ - insert:
8
+ - id: auto-memory
9
+ name: '@a9i5k4/dsh-auto-memory'
@@ -0,0 +1,455 @@
1
+ /**
2
+ * M6-3 Surface Adapter(docs/M6-CONTRACT.md §7-§11,§13 M6-3)。
3
+ * 桥接 lib/index.js(pre-step 生命周期/systemPrompt surface/调试路由)与 M6-1/2 纯核心:
4
+ * - capability 快照(不按模型名硬编码):本 DSH 构建可证明进入下一请求 messages 的
5
+ * surface=systemPrompt.context(user-role 快照追加历史尾部);pre-step/user-message patch
6
+ * 不存在 → capability='dynamic-context';连 context 都没有 → 'none'(降级 Shadow,不标 delivered)。
7
+ * - pre-step 时序(§8):bumpStep→setCursor→claim(四重门);claimed packet 缓存于 runtime 态。
8
+ * - 渲染即投递:专用 context 组件 'dsh:m6-reference-tail-pre' 的 text() 重渲染并校验 exactDigest,
9
+ * 一致则返回尾注文本(=已实际进入 messages)并 markDelivered + 异步创建 seen evidence;
10
+ * systemPrompt.section 永不承载动态 tail。
11
+ * 默认关闭(associativeMemoryEnabled ∧ activationInboxEnabled 双门);activationSource='fake' 为
12
+ * 路由注入唯一入口;'python' 仅在 assoc∧inbox∧pythonBackend 三重门下经 offerExternalActivation
13
+ * 接收 worker 帧并走现有 validator/inbox/pre-step claim(M7-1)。无 spawn/net/http;UTF-8 无 BOM。
14
+ */
15
+ import {
16
+ buildReferenceTailPacketPre, renderReferenceTail, computeExactDigest, TAIL_MARKER_LINE_V1,
17
+ REFERENCE_TAIL_BUDGET_V1, ACTIVATION_POLICY_VERSION,
18
+ } from './activation-inbox.js'
19
+ import { createActivationInboxPre, ActivationInboxRegistry, REFERENCE_TAIL_COOLDOWN_STEPS_V1 } from './activation-inbox-state.js'
20
+ import { createAccessEvidencePre } from './context-bridge.js'
21
+ import path from 'node:path'
22
+ import { buildSourceCatalog, loadCorpusSnapshot, CorpusRegistry, canonicalize } from './m4-corpus.js'
23
+
24
+ /** §11 capability 快照版本。 */
25
+ export const CAPABILITY_SNAPSHOT_V1 = 'capability_v1'
26
+
27
+ /** §11 capability 检测:按宿主提供的 surface 形状决定 packetPatch,禁止按模型名硬编码。 */
28
+ export function detectPacketCapabilityPre(ctxLike) {
29
+ const sp = ctxLike && ctxLike.systemPrompt
30
+ const hasContext = !!(sp && typeof sp.context === 'function')
31
+ return {
32
+ capabilityVersion: CAPABILITY_SNAPSHOT_V1,
33
+ packetPatch: hasContext ? 'dynamic-context' : 'none',
34
+ includeRuntimeContext: false,
35
+ maxPacketBytes: REFERENCE_TAIL_BUDGET_V1.maxPacketBytes,
36
+ supportsDeliveryAck: true, // context text 返回非空即证明进入 messages,可同步 ack
37
+ }
38
+ }
39
+
40
+ export function createActivationHost(opts = {}) {
41
+ const engine = opts.engine
42
+ if (!engine) throw new Error('activation-host: engine required')
43
+ const registry = new ActivationInboxRegistry()
44
+ const corpusRegistry = new CorpusRegistry({ sidecarDir: path.join(dshHome(), 'memory', 'index', 'files') })
45
+ const runtimeState = new Map() // runtime.key → {step, claimed:{packet, inbox}|null, identity:{sessionId,workspaceKey}|null}
46
+ let mivCache = { wsRef: null, miv: null }
47
+ const pathsByKey = new Map()
48
+ const volatileEvents = [] // ≤16 最小投影
49
+ const stats = { injected: 0, injectedAccepted: 0, injectedRejected: 0, claims: 0, claimFails: 0, rendered: 0, delivered: 0, seenCreated: 0, seenSkippedNoProv: 0, errors: 0 }
50
+
51
+ function effectiveEnabled() {
52
+ return engine.config.associativeMemoryEnabled === true && engine.config.activationInboxEnabled === true
53
+ }
54
+ /** M7-1 三重门(PYTHON-SIDECAR-CONTRACT §13.1):默认 fake 时恒不走 python 分支。 */
55
+ function pythonGate() {
56
+ return engine.config.associativeMemoryEnabled === true &&
57
+ engine.config.activationInboxEnabled === true &&
58
+ engine.config.pythonBackendEnabled === true
59
+ }
60
+ function sourceMode() {
61
+ const s = String(engine.config.activationSource || 'fake')
62
+ if (s === 'fake') return 'fake'
63
+ if (s === 'js') return 'js' // 2026-08-27 JS 判定来源(C2 检索 + JS 判定核,独立于 Python)
64
+ if (s === 'python' && pythonGate()) return 'python'
65
+ return 'invalid'
66
+ }
67
+ function dshHome() {
68
+ const env = process.env.DSH_HOME
69
+ if (env && env.trim()) return env.trim()
70
+ const base = engine.__homedirFn ? engine.__homedirFn() : (process.env.USERPROFILE || process.env.HOME || '')
71
+ return base ? path.join(base, '.dsh') : '.'
72
+ }
73
+ function pushEvent(entry) {
74
+ volatileEvents.push({ at: Date.now(), ...entry })
75
+ if (volatileEvents.length > 16) volatileEvents.shift()
76
+ }
77
+ function capturePaths(runtimeKey, p) {
78
+ pathsByKey.set(String(runtimeKey || ''), {
79
+ workspaceKey: canonicalize(p.ws),
80
+ userMemoryPath: p.userDir ? path.join(p.userDir, 'MEMORY.md') : undefined,
81
+ workspaceMemoryPath: p.notesPath,
82
+ todayLogPath: p.logPath,
83
+ })
84
+ }
85
+
86
+
87
+ /** 当前 workspace 的 memoryIndexVersion(自有 CorpusRegistry 懒加载;失败 null 并清缓存)。 */
88
+ function currentMiv(workspaceKey) {
89
+ const ws = canonicalize(workspaceKey)
90
+ const p = null
91
+ for (const [, v] of pathsByKey) {
92
+ if (v.workspaceKey !== ws) continue
93
+ try {
94
+ const catalog = buildSourceCatalog({
95
+ workspaceKey: v.workspaceKey,
96
+ userMemoryPath: v.userMemoryPath,
97
+ workspaceMemoryPath: v.workspaceMemoryPath,
98
+ todayLogPath: v.todayLogPath,
99
+ })
100
+ const res = corpusRegistry.get(catalog)
101
+ if (res && res.ok) {
102
+ mivCache = { wsRef: ws, miv: res.snapshot.memoryIndexVersion }
103
+ return mivCache.miv
104
+ }
105
+ } catch (_) {}
106
+ break
107
+ }
108
+ if (mivCache.wsRef === ws) return mivCache.miv
109
+ return null
110
+ }
111
+ function setMiv(workspaceKey, miv) { mivCache = { wsRef: canonicalize(workspaceKey), miv } }
112
+
113
+ function identityFor(runtime) {
114
+ const sessionId = runtime.sessionId || ''
115
+ const p = pathsByKey.get(String(runtime.key || '')) || null
116
+ const workspaceKey = p ? p.workspaceKey : (engine.state && engine.state.ws ? canonicalize(engine.state.ws) : '')
117
+ if (!sessionId || !workspaceKey) return null
118
+ return { sessionId, agentId: runtime.agentId || '', workspaceKey }
119
+ }
120
+
121
+ /** fake/python 注入入口(路由/未来 sink 共用)。返回 offer 结果。 */
122
+ function injectActivation(request) {
123
+ try {
124
+ if (!effectiveEnabled()) return { ok: false, reason: 'disabled' }
125
+ if (sourceMode() !== 'fake') return { ok: false, reason: 'source-not-fake' }
126
+ stats.injected++
127
+ const rv = validateRequestShape(request)
128
+ if (!rv.ok) { stats.injectedRejected++; return rv }
129
+ const req = rv.request
130
+ const box = registry.get(req.sessionId, req.workspaceKey)
131
+ if (!box) {
132
+ const created = registry.forRuntime(req.sessionId, req.workspaceKey, { contextVersion: req.contextVersion, memoryIndexVersion: req.memoryIndexVersion, agentId: req.agentId })
133
+ if (!created) { stats.injectedRejected++; return { ok: false, reason: 'identity-mismatch' } }
134
+ return finishInject(created, req)
135
+ }
136
+ if (!box.identity.agentId && req.agentId) box.identity.agentId = String(req.agentId)
137
+ return finishInject(box, req)
138
+ } catch (e) { stats.errors++; return { ok: false, reason: 'internal-error' } }
139
+ }
140
+ function finishInject(box, req) {
141
+ box.setCursor({ contextVersion: req.contextVersion, memoryIndexVersion: req.memoryIndexVersion })
142
+ const r = box.offerActivation(req, { nowStep: stepFor(req.sessionId, req.workspaceKey), currentMemoryIndexVersion: req.memoryIndexVersion })
143
+ if (r.ok) {
144
+ stats.injectedAccepted++
145
+ // fake 来源「注入即泵」:立刻以请求自声明版本过四重门 claim,并把 claimed 挂到
146
+ // 目标 runtime 态——下一次该 session 的自然 compose 会渲染尾注并 markDelivered(+seen)。
147
+ pumpClaimed(box, req)
148
+ } else stats.injectedRejected++
149
+ pushEvent({ kind: 'inject', ok: !!r.ok, outcome: r.outcome || r.reason })
150
+ return r
151
+ }
152
+ /**
153
+ * 注入即泵(2026-08-28 自 fake 提取共享):offer 成功后立刻以请求自声明版本过四重门 claim,
154
+ * claimed 挂到目标 runtime 态 → 下一次自然 compose 渲染尾注并 markDelivered(+seen)。
155
+ * 动机(live 实证):DSH 会话 cv 每段自增 + TTL=3 步 < 多步回合长度,回合中途 offer 的
156
+ * packet 活不到下一次 pre-step 自然 claim(claimFails 全 none-pending)。fake 与 JS 判定
157
+ * 来源共用;Python 保持 pre-step 自然 claim(冻结契约,待 Python live 按证据另议)。
158
+ */
159
+ function pumpClaimed(box, req) {
160
+ const c2 = box.claim({
161
+ nowStep: stepFor(req.sessionId, req.workspaceKey),
162
+ currentContextVersion: req.contextVersion,
163
+ currentMemoryIndexVersion: req.memoryIndexVersion,
164
+ })
165
+ if (c2.ok) {
166
+ stats.claims++
167
+ let target = null
168
+ for (const rt of engine.runtimes.values()) { if (rt.sessionId === req.sessionId && !rt.disposed) { target = rt; break } }
169
+ if (target) {
170
+ let st = runtimeState.get(target.key)
171
+ if (!st) { st = { step: 0, claimed: null }; runtimeState.set(target.key, st) }
172
+ st.claimed = { packet: c2.packet, inbox: box }
173
+ pushEvent({ kind: 'pump', packetId: c2.packet.packetId, runtimeKey: String(target.key).slice(0, 24) })
174
+ } else {
175
+ box.rollbackClaim()
176
+ pushEvent({ kind: 'pump', reason: 'no-runtime' })
177
+ }
178
+ } else {
179
+ pushEvent({ kind: 'pump', reason: c2.reason })
180
+ }
181
+ }
182
+ /**
183
+ * M7-1 python 来源入口(worker activation_request 帧):JS 硬校验+身份/重复/抑制/cursor/index 门后入箱。
184
+ * 与 fake 的注入即泵不同——真实 Python 推送不做 pump,走 pre-step 自然 claim(§8 时序保留;M6-4 偏差仅限 fake)。
185
+ */
186
+ function offerExternalActivation(request) {
187
+ try {
188
+ if (!effectiveEnabled()) return { ok: false, reason: 'disabled' }
189
+ const sm = sourceMode()
190
+ if (sm !== 'python' && sm !== 'js') return { ok: false, reason: 'source-not-python' }
191
+ stats.injected++
192
+ const rv = validateRequestShape(request)
193
+ if (!rv.ok) { stats.injectedRejected++; return rv }
194
+ const req = rv.request
195
+ // P0:skill 段附着。JS 档已用 query 匹配到技能时保留其结果;否则(典型=Python 档
196
+ // 无 query)退回候选∩sourceMemoryIds 匹配。技能段非法会在 M6 校验器被拒(fail-closed)。
197
+ try {
198
+ if (!req.skill) {
199
+ const sk = matchSkillByCandidates(req)
200
+ if (sk) req.skill = sk
201
+ }
202
+ } catch (_) {}
203
+ let box = registry.get(req.sessionId, req.workspaceKey)
204
+ if (!box) {
205
+ box = registry.forRuntime(req.sessionId, req.workspaceKey, { contextVersion: req.contextVersion, memoryIndexVersion: req.memoryIndexVersion, agentId: req.agentId })
206
+ if (!box) { stats.injectedRejected++; return { ok: false, reason: 'identity-mismatch' } }
207
+ }
208
+ if (!box.identity.agentId && req.agentId) box.identity.agentId = String(req.agentId)
209
+ const miv = currentMiv(req.workspaceKey)
210
+ const r = box.offerActivation(req, { nowStep: stepFor(req.sessionId, req.workspaceKey), currentMemoryIndexVersion: miv === null ? undefined : miv })
211
+ pushEvent({ kind: 'python-offer', ok: !!r.ok, outcome: r.outcome || r.reason })
212
+ if (r.ok) {
213
+ stats.injectedAccepted++
214
+ // 2026-08-30:注入即泵扩展到 Python 来源——canary 实证(JS 档 08-28)自然 claim
215
+ // 在回合中段必死(cv 每段自增+TTL3 步),Python emit 的时序机制完全相同,同一证据
216
+ // 同一修法。泵是 M6 投递侧机制,不耦合任何语义档(JS/Python 各自独立可用)。
217
+ pumpClaimed(box, req)
218
+ } else stats.injectedRejected++
219
+ return r
220
+ } catch (e) { stats.errors++; return { ok: false, reason: 'internal-error' } }
221
+ }
222
+ /**
223
+ * M9 act.skill 附着(2026-08-30 P0):Python 档 emit 帧到站时,JS 侧拿不到 query 文本
224
+ * (query 在 worker 内部,帧只带 candidates/score/identity),因此 JS 档那套「C2 稠密/词法
225
+ * 匹配技能标题」在 Python 档无解 → 改用**候选 ∩ 技能 sourceMemoryIds 求交集**匹配:
226
+ * 被唤起的记忆本身若正是某 active 技能的来源记忆,则该技能的 checklist 与本轮上下文相关。
227
+ *
228
+ * 语义分工(不可互相覆盖):
229
+ * - JS 档(context-host.js)仍走 query 匹配,命中后请求自带 skill → 此处不覆盖;
230
+ * - Python 档无 query → 走本函数的候选交集匹配。
231
+ * 纯增强:任何异常静默返回 null(无技能段),绝不阻断投递主链路。
232
+ */
233
+ function matchSkillByCandidates(req) {
234
+ try {
235
+ const hub = engine._memoryHub
236
+ if (!hub || !hub.stores || !hub.stores.procedures) return null
237
+ if (engine.config.memoryHubEnabled !== true) return null
238
+ if (engine.config.procedurePromotionEnabled === false) return null
239
+ const actives = hub.stores.procedures.activeProcedures()
240
+ if (!actives.length) return null
241
+ const candIds = new Set()
242
+ for (const c of Array.isArray(req.candidates) ? req.candidates : []) {
243
+ if (c && typeof c.memoryId === 'string' && c.memoryId) candIds.add(c.memoryId)
244
+ }
245
+ if (!candIds.size) return null
246
+ // 命中数最多者胜;平局取 store 顺序首个(过程确定,同输入同输出)
247
+ let best = null
248
+ let bestHits = 0
249
+ for (const p of actives) {
250
+ if (!p || !Array.isArray(p.sourceMemoryIds)) continue
251
+ let hits = 0
252
+ for (const id of p.sourceMemoryIds) if (candIds.has(id)) hits++
253
+ if (hits > bestHits) { bestHits = hits; best = p }
254
+ }
255
+ if (!best) return null
256
+ const cl = hub.stores.procedures.renderChecklist(best.procedureId)
257
+ if (!cl || !cl.text) return null
258
+ try { hub.stores.procedures.touch(best.procedureId) } catch (_) {} // Hermes:last_used 时钟
259
+ return {
260
+ procedureId: String(best.procedureId),
261
+ title: String((cl && cl.title) || best.title || ''),
262
+ level: String((cl && cl.level) || 'checklist'),
263
+ text: String(cl.text).slice(0, 1200),
264
+ }
265
+ } catch (_) { return null }
266
+ }
267
+
268
+ function validateRequestShape(request) {
269
+ if (!request || typeof request !== 'object') return { ok: false, reason: 'not-object' }
270
+ if (typeof request.activationId !== 'string' || !request.activationId) return { ok: false, reason: 'no-activation-id' }
271
+ return { ok: true, request }
272
+ }
273
+ const stepsByRuntime = new Map()
274
+ function stepFor(sessionId, workspaceKey) {
275
+ const key = sessionId + '|ws:' + workspaceKey
276
+ const n = (stepsByRuntime.get(key) || 0) + 1
277
+ stepsByRuntime.set(key, n)
278
+ return n
279
+ }
280
+
281
+ /**
282
+ * agent/pre-step(§8):先校验当前 cursor/index/TTL 再 claim;claimed packet 存 runtime 态,
283
+ * 等待渲染面(text())消费。cursor/miv 取自 runtime 与 corpus 快照。
284
+ */
285
+ function onPreStep(agent) {
286
+ try {
287
+ if (!effectiveEnabled()) return
288
+ const runtime = engine.runtimeFor(agent)
289
+ if (!runtime || runtime.disposed) return
290
+ const identity = identityFor(runtime)
291
+ if (!identity) { pushEvent({ kind: 'prestep', reason: 'no-identity' }); return }
292
+ const box = registry.forRuntime(identity.sessionId, identity.workspaceKey, { contextVersion: runtime.contextVersion })
293
+ if (!box) return
294
+ const miv = currentMiv(identity.workspaceKey)
295
+ box.setCursor({ contextVersion: runtime.contextVersion, memoryIndexVersion: miv || undefined })
296
+ const nowStep = stepFor(identity.sessionId, identity.workspaceKey)
297
+ const c = box.claim({
298
+ nowStep,
299
+ currentContextVersion: runtime.contextVersion,
300
+ currentMemoryIndexVersion: miv === null ? undefined : miv,
301
+ })
302
+ let st = runtimeState.get(runtime.key)
303
+ if (!st) { st = { step: 0, claimed: null }; runtimeState.set(runtime.key, st) }
304
+ st.step = nowStep
305
+ if (c.ok) { st.claimed = { packet: c.packet, inbox: box }; stats.claims++ }
306
+ else {
307
+ // 2026-08-28 浏览器实测修复:claim 失败(none-pending 等)不得清掉已存在的 claimed——
308
+ // 注入即泵挂上的包在 pending 里已不存在,下一次 pre-step 的 none-pending 失败会把
309
+ // 它擦掉,导致 rendered 永远为 0。仅在无 claimed 时保持空位。
310
+ stats.claimFails++; pushEvent({ kind: 'claim', reason: c.reason })
311
+ if (process.env.DSH_ACT_DEBUG) console.error('[act-diag] claim fail ' + c.reason + ' runtimeCv=' + runtime.contextVersion + ' miv=' + miv)
312
+ }
313
+ } catch (e) { stats.errors++ }
314
+ }
315
+
316
+ /**
317
+ * 专用渲染面(dsh:m6-reference-tail-pre 的 text()):重渲染 claimed packet 并校验 exactDigest,
318
+ * 一致 → 返回尾注文本 + markDelivered + 异步 seen;不一致/未声明 → 返回空串(零注入)。
319
+ */
320
+ function renderTailFor(agent) {
321
+ try {
322
+ if (!effectiveEnabled()) return ''
323
+ const runtime = engine.runtimeFor(agent)
324
+ if (!runtime) return ''
325
+ const st = runtimeState.get(runtime.key)
326
+ if (!st || !st.claimed) return ''
327
+ const { packet, inbox } = st.claimed
328
+ // skill 必须与 packet 构建时同源传入:渲染器据此复现逐字节一致的文本,exactDigest 才对得上
329
+ const re = renderReferenceTail(packet.references, { reason: packet.triggerReason, budgetBytes: REFERENCE_TAIL_BUDGET_V1.maxPacketBytes, skill: packet.skill })
330
+ if (!re.ok || computeExactDigest(re.text) !== packet.exactDigest) {
331
+ stats.errors++
332
+ pushEvent({ kind: 'render', reason: 'digest-mismatch' })
333
+ st.claimed = null
334
+ return ''
335
+ }
336
+ const ack = inbox.markDelivered(packet.packetId, { nowStep: st.step })
337
+ st.claimed = null
338
+ if (!ack.ok) return ''
339
+ stats.rendered++
340
+ stats.delivered++
341
+ // G-02:渲染结果可观测——skill 字段让 /activation status 端点直接证明技能段进了 tail
342
+ pushEvent({ kind: 'render', skill: !!(packet && packet.skill), refs: Array.isArray(packet.references) ? packet.references.length : 0, packetId: packet.packetId })
343
+ void createSeenEvidences(runtime, packet)
344
+ return re.text
345
+ } catch (e) { stats.errors++; return '' }
346
+ }
347
+
348
+ /** delivery ack → M5 seen evidence(provenance 从 corpus 补全;查不到则跳过,fail closed)。 */
349
+ async function createSeenEvidences(runtime, packet) {
350
+ try {
351
+ const ch = engine._contextHost
352
+ const identity = identityFor(runtime)
353
+ if (!ch || !identity) { stats.seenSkippedNoProv += packet.references.length; return }
354
+ const evidences = []
355
+ for (const ref of packet.references) {
356
+ const prov = ch.findProvenance(identity.workspaceKey, ref.memoryId, ref.recordDigest)
357
+ if (!prov) { stats.seenSkippedNoProv++; continue }
358
+ const ev = createAccessEvidencePre({
359
+ kind: 'seen', memoryId: prov.memoryId, anchorId: prov.anchorId, scope: prov.scope,
360
+ workspaceKey: identity.workspaceKey, sessionId: identity.sessionId,
361
+ eventSeq: runtime.eventCursor | 0, nativeSeq: undefined, contextVersion: packet.contextVersion,
362
+ ts: Date.now(),
363
+ sourceRef: prov.sourceRef, sourceEpoch: prov.sourceEpoch, sourceVersion: prov.sourceVersion,
364
+ fileDigest: prov.fileDigest, recordDigest: prov.recordDigest,
365
+ })
366
+ if (ev.ok) evidences.push(ev.evidence)
367
+ }
368
+ if (evidences.length) {
369
+ await ch.appendEvidence(evidences)
370
+ stats.seenCreated += evidences.length
371
+ }
372
+ } catch (e) { stats.errors++ }
373
+ }
374
+
375
+ /** §11 capability(按当前宿主形状);Host 无 ctx 引用,由 index.js 注入一次。 */
376
+ let capability = null
377
+ function initCapability(ctxLike) { capability = detectPacketCapabilityPre(ctxLike); return capability }
378
+ function getCapability() { return capability || { capabilityVersion: CAPABILITY_SNAPSHOT_V1, packetPatch: 'none', supportsDeliveryAck: false } }
379
+
380
+ function debugView() {
381
+ if (!effectiveEnabled()) return { enabled: false }
382
+ const cap = getCapability()
383
+ return {
384
+ enabled: true,
385
+ activationPolicyVersion: ACTIVATION_POLICY_VERSION,
386
+ cooldownSteps: REFERENCE_TAIL_COOLDOWN_STEPS_V1,
387
+ capability: cap,
388
+ sourceMode: sourceMode(),
389
+ memoryIndexVersion: mivCache.miv || null,
390
+ inboxCount: registry.size,
391
+ stats: { ...stats },
392
+ recentEvents: volatileEvents.slice(-4),
393
+ }
394
+ }
395
+
396
+ /**
397
+ * M10 存储管理级联清理(2026-08-30 P3):一条记忆被删除后,清掉三处在途/在册痕迹,
398
+ * 避免「已删除的记忆又被投递一次」:
399
+ * ①每个 inbox:进抑制名单 + 丢弃含它的 pending 包(见 inbox.purgeMemoryId)
400
+ * ②已挂到 runtime 态的 claimed 包(注入即泵挂上的):含它则丢弃 → 下一轮渲染为空
401
+ * ③already-delivered 的 seen 证据不动(seen 语义冻结:已发生的事实不改写)
402
+ * 纯内存操作,零 IO;不影响 validator/Reference Tail 固定边界/seen 语义。
403
+ */
404
+ function purgeMemory(memoryId) {
405
+ const id = String(memoryId || '')
406
+ if (!id) return { ok: false, reason: 'no-memory-id' }
407
+ let inboxCount = 0
408
+ let droppedPending = 0
409
+ let droppedClaimed = 0
410
+ try {
411
+ for (const box of registry.boxes()) {
412
+ inboxCount++
413
+ const r = box.purgeMemoryId(id)
414
+ if (r && r.droppedPending) droppedPending++
415
+ }
416
+ for (const [key, st] of runtimeState) {
417
+ if (!st || !st.claimed) continue
418
+ const refs = st.claimed.packet && Array.isArray(st.claimed.packet.references) ? st.claimed.packet.references : []
419
+ if (refs.some((r) => r && r.memoryId === id)) { st.claimed = null; droppedClaimed++ }
420
+ void key
421
+ }
422
+ pushEvent({ kind: 'purge', memoryId: id, inboxCount, droppedPending, droppedClaimed })
423
+ return { ok: true, inboxCount, droppedPending, droppedClaimed }
424
+ } catch (e) { stats.errors++; return { ok: false, reason: 'internal-error' } }
425
+ }
426
+
427
+ function disposeRuntime(runtimeKey) {
428
+ runtimeState.delete(runtimeKey)
429
+ stepsByRuntime.delete(String(runtimeKey))
430
+ }
431
+ function disposeAll(reason) {
432
+ runtimeState.clear()
433
+ stepsByRuntime.clear()
434
+ registry.disposeAll(reason)
435
+ volatileEvents.length = 0
436
+ }
437
+ function disposeSession(sessionId, workspaceKey) { registry.disposeSession(sessionId, workspaceKey) }
438
+
439
+ return {
440
+ initCapability,
441
+ getCapability,
442
+ capturePaths,
443
+ effectiveEnabled,
444
+ injectActivation,
445
+ offerExternalActivation,
446
+ purgeMemory,
447
+ onPreStep,
448
+ renderTailFor,
449
+ debugView,
450
+ disposeRuntime,
451
+ disposeAll,
452
+ disposeSession,
453
+ _statsForTest: stats,
454
+ }
455
+ }