@a9i5k4/dsh-auto-memory 0.1.29 → 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 +1379 -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 +2357 -102
- 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,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M4-3 Host Shadow Wiring(docs/M4-CONTRACT.md §5/§14/§15/§17)。
|
|
3
|
+
* 桥接 lib/index.js(M2 ContextObserver/M3 sidecar)与 M4-1 纯核心:
|
|
4
|
+
* - per-runtime Shadow state(WeakMap,lazy;enableEpoch/completedKeys/recentHits/cooldown/latch)
|
|
5
|
+
* - accepted Segment → 同步捕获 paths 快照 → 异步 latest-wins 调度(gate→corpus→lexical→audit)
|
|
6
|
+
* - durable audit:<DSH_HOME>/memory/retrieval-pre/audit/YYYY-MM-DD.jsonl(engine 级串行,32KiB 截断,
|
|
7
|
+
* 隐私投影:无原文/无绝对路径/无 sessionId/term 只存 digest)+ retention(14 天/32MiB)
|
|
8
|
+
* - debugView(§17 最小投影;关闭时严格 {enabled:false})
|
|
9
|
+
* 默认关闭:三开关任一为 false 时零构造、零 IO、零留存。
|
|
10
|
+
*/
|
|
11
|
+
import { mkdirSync, appendFileSync, writeFileSync, readFileSync, readdirSync, existsSync, statSync, rmSync } from 'node:fs'
|
|
12
|
+
import path from 'node:path'
|
|
13
|
+
import { homedir } from 'node:os'
|
|
14
|
+
import { createHash } from 'node:crypto'
|
|
15
|
+
import {
|
|
16
|
+
validateSnapshot, buildQueryPlan, computeSignals, gatePreV1,
|
|
17
|
+
lexicalSearch, buildCandidates, buildRetrievalId, replay as replayCore,
|
|
18
|
+
SHADOW_GATE_POLICY_V1, LEXICAL_POLICY_VERSION, GATE_POLICY_VERSION, NAMESPACE,
|
|
19
|
+
} from './shadow-retrieval.js'
|
|
20
|
+
import { buildSourceCatalog, loadCorpusSnapshot, CorpusRegistry, canonicalize } from './m4-corpus.js'
|
|
21
|
+
|
|
22
|
+
const sha256Hex = (buf) => createHash('sha256').update(buf).digest('hex')
|
|
23
|
+
const first32 = (s) => String(s || '').slice(0, 32)
|
|
24
|
+
const AUDIT_KEEP_DAYS = 14
|
|
25
|
+
const AUDIT_MAX_BYTES = 32 * 1024 * 1024
|
|
26
|
+
const AUDIT_EVENT_MAX = 32 * 1024
|
|
27
|
+
|
|
28
|
+
/** 单 audit 事件 32KiB 上限:超限按 candidate/drop 尾部裁剪并标记。 */
|
|
29
|
+
export function truncateAuditEvent(ev) {
|
|
30
|
+
let e = ev
|
|
31
|
+
if (Buffer.byteLength(JSON.stringify(e), 'utf8') <= AUDIT_EVENT_MAX) return e
|
|
32
|
+
e = JSON.parse(JSON.stringify(ev))
|
|
33
|
+
e.auditTruncated = true
|
|
34
|
+
while (Buffer.byteLength(JSON.stringify(e), 'utf8') > AUDIT_EVENT_MAX && (e.candidates.length || e.dropped.length)) {
|
|
35
|
+
if (e.candidates.length >= e.dropped.length) e.candidates.pop()
|
|
36
|
+
else e.dropped.pop()
|
|
37
|
+
e.counts.kept = e.candidates.length
|
|
38
|
+
}
|
|
39
|
+
return e
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createShadowHost({ engine }) {
|
|
43
|
+
const states = new WeakMap() // runtime → shadow state(lazy)
|
|
44
|
+
const volatileRing = [] // ≤64 events(最小投影)
|
|
45
|
+
const stats = { evaluated: 0, retrieved: 0, prefetched: 0, suppressed: 0, stale: 0, errors: 0 }
|
|
46
|
+
const registry = new CorpusRegistry({ sidecarDir: path.join(dshHome(), 'memory', 'index', 'files') })
|
|
47
|
+
let lastMemoryIndexVersion = null
|
|
48
|
+
let lastCombo = null
|
|
49
|
+
let enableEpoch = 0
|
|
50
|
+
let lastGate = null
|
|
51
|
+
let inFlightCount = 0
|
|
52
|
+
let lastAuditAt = 0
|
|
53
|
+
let auditSwept = false
|
|
54
|
+
|
|
55
|
+
const effectiveEnabled = () =>
|
|
56
|
+
engine.config.associativeMemoryEnabled === true &&
|
|
57
|
+
engine.config.shadowRetrievalEnabled === true &&
|
|
58
|
+
engine.config.memoryAnchorEnabled === true
|
|
59
|
+
|
|
60
|
+
function stateFor(runtime) {
|
|
61
|
+
let st = states.get(runtime)
|
|
62
|
+
if (!st) {
|
|
63
|
+
st = {
|
|
64
|
+
enableEpoch, processedSegmentCount: 0, cooldownUntilSegment: 0,
|
|
65
|
+
latched: false, recentHits: [], completedKeys: new Set(),
|
|
66
|
+
ignoredDigests: [], inFlight: null, runtimeTag: null, lastGate: null,
|
|
67
|
+
}
|
|
68
|
+
states.set(runtime, st)
|
|
69
|
+
}
|
|
70
|
+
return st
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** §17 debugView:关闭时严格 {enabled:false}。 */
|
|
74
|
+
function debugView() {
|
|
75
|
+
if (!effectiveEnabled()) return { enabled: false }
|
|
76
|
+
const runtimes = []
|
|
77
|
+
for (const pair of collectStates()) {
|
|
78
|
+
const st = pair.state
|
|
79
|
+
runtimes.push({
|
|
80
|
+
runtimeTag: st.runtimeTag ? st.runtimeTag.slice(0, 8) : undefined,
|
|
81
|
+
contextVersion: pair.runtime.contextVersion,
|
|
82
|
+
gate: st.lastGate ? { action: st.lastGate.action, hesitation: st.lastGate.hesitation } : null,
|
|
83
|
+
inFlight: !!st.inFlight,
|
|
84
|
+
processedSegmentCount: st.processedSegmentCount,
|
|
85
|
+
enableEpoch: st.enableEpoch,
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
enabled: true, shadowOnly: true,
|
|
90
|
+
gatePolicyVersion: GATE_POLICY_VERSION, lexicalPolicyVersion: LEXICAL_POLICY_VERSION,
|
|
91
|
+
memoryIndexVersion: lastMemoryIndexVersion,
|
|
92
|
+
corpus: { sources: lastCorpusCounts.sources, records: lastCorpusCounts.records, legacyConflicts: lastCorpusCounts.legacyConflicts, staleSources: lastCorpusCounts.staleSources || 0 },
|
|
93
|
+
stats: { ...stats },
|
|
94
|
+
lastAuditError,
|
|
95
|
+
auditDirPath: auditDir(),
|
|
96
|
+
auditWritten,
|
|
97
|
+
runtimes,
|
|
98
|
+
recentAudit: volatileRing.slice(-3),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function collectStates() {
|
|
103
|
+
// WeakMap 不可枚举:debug 用 engine.runtimes 反查
|
|
104
|
+
const out = []
|
|
105
|
+
for (const rt of engine.runtimes.values()) {
|
|
106
|
+
const st = states.get(rt)
|
|
107
|
+
if (st) out.push({ runtime: rt, state: st })
|
|
108
|
+
}
|
|
109
|
+
return out
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const lastCorpusCounts = { sources: 0, records: 0, legacyConflicts: 0, staleSources: 0 }
|
|
113
|
+
|
|
114
|
+
/** 调度瞬间同步捕获的 paths 快照由 index.js 写入(refresh 完成时)。 */
|
|
115
|
+
const pathsByKey = new Map()
|
|
116
|
+
function capturePaths(runtimeKey, p) {
|
|
117
|
+
pathsByKey.set(String(runtimeKey || ''), {
|
|
118
|
+
workspaceKey: canonicalize(p.ws),
|
|
119
|
+
// engine.state 无 userFile 字段(§refresh 只写 userDir):用户级=state.userDir/MEMORY.md(与 resolvePaths 公式一致)
|
|
120
|
+
userMemoryPath: p.userDir ? path.join(p.userDir, 'MEMORY.md') : undefined,
|
|
121
|
+
workspaceMemoryPath: p.notesPath,
|
|
122
|
+
todayLogPath: p.logPath,
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** durable audit 目录与日期分片。 */
|
|
127
|
+
function auditDir() { return path.join(dshHome(), 'memory', 'retrieval-pre', 'audit') }
|
|
128
|
+
function dshHome() {
|
|
129
|
+
const env = process.env.DSH_HOME
|
|
130
|
+
if (env && env.trim()) return env.trim()
|
|
131
|
+
// M4-4 修复:必须拼接 .dsh(此前漏拼导致 audit 写到 <home>/memory 错误位置)
|
|
132
|
+
const base = engine.__homedirFn ? engine.__homedirFn() : (process.env.USERPROFILE || process.env.HOME || '')
|
|
133
|
+
return base ? path.join(base, '.dsh') : '.'
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 串行 durable append(§15.4):engine 级链式;失败只计 audit-write-failed 不重试不污染 Session。 */
|
|
137
|
+
let auditChainTail = Promise.resolve()
|
|
138
|
+
let auditWritten = 0
|
|
139
|
+
let lastAuditError = null
|
|
140
|
+
function appendAuditDurable(event) {
|
|
141
|
+
const trimmed = truncateAuditEvent(event)
|
|
142
|
+
const date = new Date(event.recordedAt)
|
|
143
|
+
const fname = date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0') + '.jsonl'
|
|
144
|
+
auditChainTail = auditChainTail.then(() => {
|
|
145
|
+
try {
|
|
146
|
+
const dir = auditDir()
|
|
147
|
+
mkdirSync(dir, { recursive: true })
|
|
148
|
+
appendFileSync(path.join(dir, fname), JSON.stringify(trimmed) + '\n', 'utf8')
|
|
149
|
+
maybeRetentionSweep(dir)
|
|
150
|
+
auditWritten++
|
|
151
|
+
return true
|
|
152
|
+
} catch (e2) {
|
|
153
|
+
lastAuditError = 'audit-write-failed:' + (e2 && e2.message ? e2.message : String(e2))
|
|
154
|
+
try { console.error('[shadow-audit] ' + lastAuditError) } catch (_) {}
|
|
155
|
+
return false
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
return auditChainTail
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** retention:保留 14 天且总量 ≤32MiB;只清 audit 分片,不动 Markdown/sidecar。 */
|
|
162
|
+
function maybeRetentionSweep(dir) {
|
|
163
|
+
if (auditSwept) return
|
|
164
|
+
auditSwept = true
|
|
165
|
+
try {
|
|
166
|
+
const now = Date.now()
|
|
167
|
+
let total = 0
|
|
168
|
+
const files = []
|
|
169
|
+
for (const f of readdirSync(dir)) {
|
|
170
|
+
if (!f.endsWith('.jsonl')) continue
|
|
171
|
+
const fp = path.join(dir, f)
|
|
172
|
+
const st = statSync(fp)
|
|
173
|
+
total += st.size
|
|
174
|
+
files.push({ fp, mtimeMs: st.mtimeMs, size: st.size })
|
|
175
|
+
}
|
|
176
|
+
for (const f of files) {
|
|
177
|
+
const ageDays = (now - f.mtimeMs) / 86400000
|
|
178
|
+
if (ageDays > AUDIT_KEEP_DAYS || total > AUDIT_MAX_BYTES) {
|
|
179
|
+
try { rmSyncSafe(f.fp); total -= f.size } catch (_) {}
|
|
180
|
+
}
|
|
181
|
+
if (total <= AUDIT_MAX_BYTES && ageDays <= AUDIT_KEEP_DAYS) break
|
|
182
|
+
}
|
|
183
|
+
} catch (_) {}
|
|
184
|
+
}
|
|
185
|
+
function rmSyncSafe(p) { try { rmSync(p, { force: true }) } catch (_) {} }
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
init() { /* 兼容占位:运行期依赖已静态化 */ },
|
|
189
|
+
capturePaths,
|
|
190
|
+
effectiveEnabled,
|
|
191
|
+
isLive: effectiveEnabled,
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* M2 Segment accept 后调用(index.js ingestEnvelope 内 fire-and-forget)。
|
|
195
|
+
* 同步段:paths 快照/snapshot 构造/gate/latest-wins abort;异步段:corpus+rank+audit。
|
|
196
|
+
*/
|
|
197
|
+
onSegmentAccepted(runtime, seg, envelope) {
|
|
198
|
+
try {
|
|
199
|
+
const combo = [
|
|
200
|
+
engine.config.associativeMemoryEnabled === true,
|
|
201
|
+
engine.config.shadowRetrievalEnabled === true,
|
|
202
|
+
engine.config.memoryAnchorEnabled === true,
|
|
203
|
+
]
|
|
204
|
+
const comboStr = combo.join(',')
|
|
205
|
+
if (lastCombo !== null && comboStr !== lastCombo) {
|
|
206
|
+
enableEpoch++
|
|
207
|
+
states.delete(runtime) // 简化:该 runtime 状态清零(其它 runtime 下次触发时同样处理)
|
|
208
|
+
}
|
|
209
|
+
lastCombo = comboStr
|
|
210
|
+
if (!effectiveEnabled()) return // 关闭零构造零留存
|
|
211
|
+
const st = stateFor(runtime)
|
|
212
|
+
st.processedSegmentCount += 1
|
|
213
|
+
if (runtime.disposed) return
|
|
214
|
+
const cooldownRemaining = Math.max(0, st.cooldownUntilSegment - st.processedSegmentCount)
|
|
215
|
+
// child session hard suppress(§6:parentSession 存在即 child;volatile counter only)
|
|
216
|
+
const isChild = !!(runtime.agent && runtime.agent.session && runtime.agent.session.header && runtime.agent.session.header.parentSession)
|
|
217
|
+
if (isChild) {
|
|
218
|
+
stats.suppressed++
|
|
219
|
+
pushVolatile({ reason: 'child-session', contextVersion: seg.contextVersion })
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
// plugin-generated user trigger 不得单独触发 retrieve(§6);标量在 envelope.payload 最小投影
|
|
223
|
+
const envPayload = (envelope && envelope.payload) || {}
|
|
224
|
+
if (seg.kind === 'user' && envPayload.sourcePlugin) {
|
|
225
|
+
stats.suppressed++
|
|
226
|
+
pushVolatile({ reason: 'plugin-generated-trigger', contextVersion: seg.contextVersion })
|
|
227
|
+
return
|
|
228
|
+
}
|
|
229
|
+
const paths = pathsByKey.get(String(runtime.key || '')) || null
|
|
230
|
+
if (!paths) { console.error('[shadow-diag] no-paths'); stats.suppressed++; pushVolatile({ reason: 'no-paths-captured', contextVersion: seg.contextVersion }); return }
|
|
231
|
+
// window:segments ring 最近 8 条(含当前)
|
|
232
|
+
const ringItems = runtime.segments ? runtime.segments.snapshot() : []
|
|
233
|
+
const win = ringItems.slice(-8).map((w) => ({
|
|
234
|
+
segmentId: w.id, digest: w.digest, kind: w.kind, eventSeq: w.eventSeq,
|
|
235
|
+
contextVersion: w.contextVersion, ts: w.ts, text: w.text,
|
|
236
|
+
toolName: w.toolName != null ? w.toolName : null,
|
|
237
|
+
toolOk: w.toolOk != null ? w.toolOk : null,
|
|
238
|
+
errorName: w.errorName != null ? w.errorName : null,
|
|
239
|
+
errorCode: w.errorCode != null ? w.errorCode : null,
|
|
240
|
+
}))
|
|
241
|
+
const snap = validateSnapshot({
|
|
242
|
+
schemaVersion: 1, sessionId: envelope.sessionId || '', agentId: runtime.agentId || '',
|
|
243
|
+
workspaceKey: paths.workspaceKey, sessionClass: 'top-level',
|
|
244
|
+
contextVersion: runtime.contextVersion, eventSeq: seg.eventSeq,
|
|
245
|
+
trigger: {
|
|
246
|
+
segmentId: seg.id, segmentDigest: seg.digest, kind: seg.kind, eventType: seg.eventType,
|
|
247
|
+
nativeSeq: seg.nativeSeq != null ? seg.nativeSeq : undefined, ts: seg.ts,
|
|
248
|
+
text: seg.text,
|
|
249
|
+
inputSource: envPayload.inputSource || null,
|
|
250
|
+
sourcePlugin: envPayload.sourcePlugin || null,
|
|
251
|
+
toolName: seg.toolName != null ? seg.toolName : null,
|
|
252
|
+
toolOk: seg.toolOk != null ? seg.toolOk : null,
|
|
253
|
+
errorName: seg.errorName != null ? seg.errorName : null,
|
|
254
|
+
errorCode: seg.errorCode != null ? seg.errorCode : null,
|
|
255
|
+
callId: (envelope && envelope.callId) || null,
|
|
256
|
+
rootCallId: (envelope && envelope.rootCallId) || null,
|
|
257
|
+
},
|
|
258
|
+
window: win,
|
|
259
|
+
})
|
|
260
|
+
if (!snap.ok) { stats.suppressed++; pushVolatile({ reason: snap.reason, contextVersion: runtime.contextVersion }); return }
|
|
261
|
+
const snapshot = snap.snapshot
|
|
262
|
+
const qp = buildQueryPlan(snapshot)
|
|
263
|
+
const dec = gatePreV1(snapshot, { previousLatch: st.latched, cooldownRemaining, signals: computeSignals(snapshot, qp, st.recentHits), queryPlan: qp })
|
|
264
|
+
st.lastGate = { action: dec.action, hesitation: dec.hesitation, rawScore: dec.rawScore, latched: dec.latched }
|
|
265
|
+
st.latched = dec.latched
|
|
266
|
+
st.cooldownUntilSegment = dec.action === 'retrieve' ? (st.processedSegmentCount + SHADOW_GATE_POLICY_V1.cooldownSegments) : st.cooldownUntilSegment
|
|
267
|
+
stats.evaluated++
|
|
268
|
+
if (dec.action === 'suppress') { stats.suppressed++; pushSuppressed(dec, snapshot); return }
|
|
269
|
+
// latest-wins abort
|
|
270
|
+
if (st.inFlight && st.inFlight.contextVersion !== snapshot.contextVersion) {
|
|
271
|
+
try { st.inFlight.controller.abort() } catch (_) {}
|
|
272
|
+
stats.stale++
|
|
273
|
+
}
|
|
274
|
+
const controller = new AbortController()
|
|
275
|
+
const retrievalIdSeed = { sessionId: snapshot.sessionId, contextVersion: snapshot.contextVersion, segmentId: snapshot.trigger.segmentId }
|
|
276
|
+
st.inFlight = { contextVersion: snapshot.contextVersion, controller, retrievalIdSeed }
|
|
277
|
+
inFlightCount++
|
|
278
|
+
void runRetrieval({ runtime, st, snapshot, qp, dec, cat: buildSourceCatalog(paths), controller, retrievalIdSeed })
|
|
279
|
+
.catch((e) => { stats.errors++; pushVolatile({ reason: 'internal-error', detail: e && e.message }) })
|
|
280
|
+
} catch (e) { stats.errors++ }
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
debugView,
|
|
284
|
+
getStats: () => ({ ...stats }),
|
|
285
|
+
disposeRuntime(runtime) {
|
|
286
|
+
const st = states.get(runtime)
|
|
287
|
+
if (st && st.inFlight) { try { st.inFlight.controller.abort() } catch (_) {} ; st.inFlight = null }
|
|
288
|
+
states.delete(runtime)
|
|
289
|
+
},
|
|
290
|
+
disposeAll(reason) {
|
|
291
|
+
for (const pair of collectStates()) this.disposeRuntime(pair.runtime)
|
|
292
|
+
volatileRing.length = 0
|
|
293
|
+
},
|
|
294
|
+
replayFromFile,
|
|
295
|
+
_volatileRing: volatileRing,
|
|
296
|
+
_statsForTest: stats,
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function pushVolatile(entry) {
|
|
300
|
+
volatileRing.push({ at: Date.now(), ...entry })
|
|
301
|
+
if (volatileRing.length > 64) volatileRing.shift()
|
|
302
|
+
}
|
|
303
|
+
function pushSuppressed(dec, snapshot) {
|
|
304
|
+
pushVolatile({ reason: dec.reason, action: 'suppress', contextVersion: snapshot.contextVersion, hesitation: dec.hesitation })
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
async function runRetrieval(ctxR) {
|
|
308
|
+
const { runtime, st, snapshot, qp, dec, controller } = ctxR
|
|
309
|
+
if (controller.signal.aborted) { stats.stale++; return }
|
|
310
|
+
const catalog = ctxR.cat
|
|
311
|
+
const corpusRes = registry.get(catalog)
|
|
312
|
+
if (!corpusRes.ok) { stats.suppressed++; pushVolatile({ reason: corpusRes.reason }); return }
|
|
313
|
+
const snapshotOut = corpusRes.snapshot
|
|
314
|
+
if (controller.signal.aborted) { stats.stale++; return }
|
|
315
|
+
lastMemoryIndexVersion = snapshotOut.memoryIndexVersion
|
|
316
|
+
lastCorpusCounts.sources = snapshotOut.counts.sources
|
|
317
|
+
lastCorpusCounts.records = snapshotOut.records.length
|
|
318
|
+
const ls = lexicalSearch(snapshotOut, qp, { triggerTs: snapshot.trigger.ts, mode: dec.action === 'retrieve' ? 'retrieve' : 'prefetch', dayBoundaryMinutes: Number(engine.config.dayBoundaryMinutes) || 450 })
|
|
319
|
+
if (controller.signal.aborted) { stats.stale++; return }
|
|
320
|
+
const retrievalId = buildRetrievalId(snapshot.sessionId, snapshot.contextVersion, snapshot.trigger.segmentId, snapshotOut.memoryIndexVersion)
|
|
321
|
+
const candidates = buildCandidates(retrievalId, ls.kept, dec.action === 'retrieve' ? 'retrieve' : 'prefetch')
|
|
322
|
+
if (dec.action === 'retrieve') stats.retrieved++
|
|
323
|
+
else stats.prefetched++
|
|
324
|
+
st.recentHits.push({ queryDigest: qp.queryDigest, fresh: ls.kept.length > 0, memoryIds: ls.kept.map((k) => k.memoryId) })
|
|
325
|
+
if (st.recentHits.length > 64) st.recentHits.shift()
|
|
326
|
+
st.completedKeys.add(qp.queryDigest + ':' + snapshotOut.memoryIndexVersion)
|
|
327
|
+
if (st.completedKeys.size > 256) { const first = st.completedKeys.values().next().value; st.completedKeys.delete(first) }
|
|
328
|
+
// durable audit(§15.2 schema,隐私投影)
|
|
329
|
+
const ev = truncateAuditEvent({
|
|
330
|
+
schemaVersion: 1, namespace: NAMESPACE,
|
|
331
|
+
retrievalId, recordedAt: Date.now(), triggerTs: snapshot.trigger.ts,
|
|
332
|
+
contextVersion: snapshot.contextVersion, eventSeq: snapshot.eventSeq,
|
|
333
|
+
triggerSegmentId: snapshot.trigger.segmentId, triggerSegmentDigest: snapshot.trigger.segmentDigest,
|
|
334
|
+
triggerKind: snapshot.trigger.kind,
|
|
335
|
+
memoryIndexVersion: snapshotOut.memoryIndexVersion,
|
|
336
|
+
gatePolicyVersion: GATE_POLICY_VERSION, lexicalPolicyVersion: LEXICAL_POLICY_VERSION,
|
|
337
|
+
queryDigest: qp.queryDigest,
|
|
338
|
+
gate: { action: dec.action, state: dec.state, reason: dec.reason, rawScore: dec.rawScore, hesitation: dec.hesitation, signals: dec.signals },
|
|
339
|
+
outcome: candidates.length ? 'completed' : 'empty',
|
|
340
|
+
candidates: candidates.map((c) => ({
|
|
341
|
+
candidateId: c.candidateId, memoryId: c.memoryId, anchorId: c.anchorId,
|
|
342
|
+
scope: c.scope, sourceClass: c.sourceClass, sourceRef: c.sourceRef,
|
|
343
|
+
sourceEpoch: c.sourceEpoch, sourceVersion: c.sourceVersion, fileDigest: c.fileDigest, recordDigest: c.recordDigest,
|
|
344
|
+
score: c.scores.total, reasonCodes: c.reasonCodes,
|
|
345
|
+
})),
|
|
346
|
+
dropped: ls.dropped.map((d) => ({ stage: d.stage, reason: d.reason, memoryId: d.memoryId, sourceRef: d.sourceRef })),
|
|
347
|
+
counts: { sources: ls.counts.sources, records: ls.counts.records, legacyConflicts: ls.counts.legacyConflicts, rawHits: ls.counts.rawHits, kept: ls.counts.kept, dropped: ls.counts.dropped },
|
|
348
|
+
latencyMs: { gate: 0, corpus: 0, search: 0, audit: 0, total: 0 },
|
|
349
|
+
shadowOnly: true, injected: false, packetId: null, delivered: false, accessEvidenceCreated: false,
|
|
350
|
+
})
|
|
351
|
+
await appendAuditDurable(ev)
|
|
352
|
+
lastAuditAt = Date.now()
|
|
353
|
+
if (st.inFlight && st.inFlight.contextVersion === snapshot.contextVersion) st.inFlight = null
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** §16 显式 replay(纯核心;persist=false 不污染 live audit)。 */
|
|
357
|
+
function replayFromFile(inputPath) {
|
|
358
|
+
const fixture = JSON.parse(readFileSync(inputPath, 'utf8'))
|
|
359
|
+
return replay(fixture)
|
|
360
|
+
}
|
|
361
|
+
}
|