@a9i5k4/dsh-auto-memory 3.0.1 → 3.1.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/README.md +13 -8
- package/README.zh-CN.md +13 -8
- package/docs/HANDBOOK.md +88 -52
- package/docs/USER-GUIDE.en.md +9 -9
- package/docs/USER-GUIDE.zh-CN.md +9 -9
- package/docs/screenshots/promo/promo-0-banner-v4.png +0 -0
- package/docs/screenshots/promo/promo-1b-auto-recall.png +0 -0
- package/lib/activation-host.js +6 -1
- package/lib/client.js +819 -272
- package/lib/context-host.js +7 -1
- package/lib/episodic-store.js +90 -16
- package/lib/fact-store.js +463 -41
- package/lib/hub-io.js +217 -0
- package/lib/index.js +224 -45
- package/lib/intent-clean-safe.js +1 -1
- package/lib/memory-hub.js +37 -5
- package/lib/note-status.js +9 -1
- package/lib/procedure-store.js +252 -31
- package/lib/procedure-switch.js +38 -0
- package/lib/python-sidecar-client.js +285 -8
- package/package.json +6 -2
- package/docs/internal/ACCEPT-35-LIVE.md +0 -143
- package/docs/internal/ACCEPTANCE-20260914.md +0 -90
- package/docs/internal/ARCH-REVIEW-BRIEF.md +0 -411
- package/docs/internal/ARCH-REVIEW-REQUEST.md +0 -201
- package/docs/internal/ARCH-REVIEW-ROUND2.md +0 -169
- package/docs/internal/ARCH-REVIEW-ROUND3.md +0 -206
- package/docs/internal/ARCHITECTURE-FOR-ZCODE-20260920.md +0 -397
- package/docs/internal/ART-DIRECTION-DEEPSEEK-20260920.md +0 -351
- package/docs/internal/ART-DIRECTION-WIREFRAME.md +0 -191
- package/docs/internal/ART-DIRECTION-WIREFRAME.md.bak-superseded +0 -181
- package/docs/internal/AUDIT-WB-GRAPH-FULL-20260916.md +0 -314
- package/docs/internal/BATTLE-PLAN-20260917.md +0 -871
- package/docs/internal/CONCURRENCY-INVESTIGATION-20260917.md +0 -192
- package/docs/internal/CROSS-SESSION-SEARCH-PATH-DECISION.md +0 -72
- package/docs/internal/CROSS-SESSION-SEARCH-RESEARCH.md +0 -131
- package/docs/internal/CUA-VISION-FIX-NOTES.md +0 -78
- package/docs/internal/DECISIONS-20260914-SESSION.md +0 -269
- package/docs/internal/DESIGN-OVERHAUL-PRE-RESEARCH.md +0 -292
- package/docs/internal/DESIGN-P1-STATE-COMMIT-20260915.md +0 -219
- package/docs/internal/DIRECTION-CHECK-WB-GRAPH-20260916.md +0 -132
- package/docs/internal/FEATURE-INVENTORY.md +0 -531
- package/docs/internal/FEEDBACK-TO-DSHAPI-RELAY.md +0 -13
- package/docs/internal/G-SERIES-EXECUTION-20260917.md +0 -248
- package/docs/internal/G3-DESIGN-20260918.md +0 -82
- package/docs/internal/G3-DISK-FORMAT-GAP-20260919.md +0 -92
- package/docs/internal/GH-DISCUSSION-5732-COMMENT.md +0 -74
- package/docs/internal/GPT-ACCEPTANCE-PROMPT-20260916.md +0 -352
- package/docs/internal/GPT-REVIEW-PROMPT.md +0 -216
- package/docs/internal/GROUP-DIGEST-SETUP.md +0 -62
- package/docs/internal/GROUP-LISTENER-SETUP.md +0 -49
- package/docs/internal/GROUP-WEBHOOK-SETUP.md +0 -93
- package/docs/internal/HANDOFF-TO-ZCODE-20260920.md +0 -309
- package/docs/internal/HANDOFF-TO-ZCODE.md +0 -168
- package/docs/internal/HERMES-DATA-VERIFICATION-20260919.md +0 -120
- package/docs/internal/HERMES-LEGACY-STATUS-20260919.md +0 -74
- package/docs/internal/ISSUE-55-58-VERIFICATION-20260918.md +0 -175
- package/docs/internal/ISSUE10-FIX-EXECUTION-20260919.md +0 -389
- package/docs/internal/ISSUE10-PLAN-20260919.md +0 -254
- package/docs/internal/ISSUE10B-FORENSICS-20260919.md +0 -468
- package/docs/internal/ISSUE9-PURGE-AND-R1-PLAIN-20260919.md +0 -150
- package/docs/internal/ISSUE9-RESIDUAL-FORENSICS-20260919.md +0 -114
- package/docs/internal/KICKOFF-P0.md +0 -254
- package/docs/internal/LESSON-TO-CANDIDATE-STATUS-20260919.md +0 -79
- package/docs/internal/MASTER-PLAN-3.0.md +0 -411
- package/docs/internal/MEMORY-GOVERNANCE-20260917.md +0 -309
- package/docs/internal/MEMORY-MUTATION-AND-INDEX-DESIGN.md +0 -85
- package/docs/internal/MERGE-CONFLICT-SCAN-20260914.md +0 -222
- package/docs/internal/NEXT-VERSION-TODO.md +0 -95
- package/docs/internal/OFFICIAL-DISCUSSION-DRAFT.md +0 -80
- package/docs/internal/PENDING-FIXES-20260916.md +0 -289
- package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md +0 -705
- package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md.bak-s10 +0 -649
- package/docs/internal/PROCEDURAL-MEMORY-AND-APPROVAL-DESIGN-20260918.md +0 -225
- package/docs/internal/PROGRESS-20260917.md +0 -93
- package/docs/internal/PROMPT-GAP-AUDIT-20260920.md +0 -128
- package/docs/internal/R1-DEGRADE-AUDIT-20260918.md +0 -163
- package/docs/internal/R1-READABILITY-FORENSICS-20260919.md +0 -127
- package/docs/internal/R2-EVIDENCE-DEEP-AUDIT-20260918.md +0 -140
- package/docs/internal/R3-DEGRADE-LEDGER-DESIGN-20260918.md +0 -138
- package/docs/internal/R4-RECALL-QUOTA-PLAN-20260918.md +0 -218
- package/docs/internal/RAG-KARPATHY-PROGRAM.md +0 -229
- package/docs/internal/RELEASE-PROCESS.md +0 -99
- package/docs/internal/REPORT-P0-NIGHTLY.md +0 -212
- package/docs/internal/REPORT-P5-ACCEPTANCE.md +0 -31
- package/docs/internal/REPORT-WB-GRAPH-NIGHTLY.md +0 -153
- package/docs/internal/RESUME-20260918.md +0 -171
- package/docs/internal/RESUME-20260919.md +0 -104
- package/docs/internal/REVIEW-WB-GRAPH-SELF.md +0 -81
- package/docs/internal/RHINELAB-TO-DEEPSEEK-FEASIBILITY.md +0 -198
- package/docs/internal/ROADMAP-20260917-WEEK.md +0 -439
- package/docs/internal/ROADMAP.md +0 -106
- package/docs/internal/RUN-P0-NIGHTLY.md +0 -227
- package/docs/internal/S10-CONSTRUCTION-HANDOFF-20260917.md +0 -185
- package/docs/internal/S10-GAP-INVENTORY-20260917.md +0 -239
- package/docs/internal/S10-GAPS-PLAIN-20260917.md +0 -125
- package/docs/internal/SEMANTIC-ARCHITECTURE-SPEC.md +0 -360
- package/docs/internal/SESSION-FILE-REPAIR-PROTOCOL.md +0 -90
- package/docs/internal/SUBAGENT-REPORT-ROUTING-PRE-RESEARCH.md +0 -261
- package/docs/internal/T6-EXECUTION-20260920.md +0 -130
- package/docs/internal/TELEMETRY-EFFECT-REPORT-DESIGN-20260918.md +0 -146
- package/docs/internal/THESIS-GAP-ANALYSIS-20260918.md +0 -89
- package/docs/internal/THESIS-OUTLINE-20260918.md +0 -147
- package/docs/internal/THREE-LAYER-CONTRACT.md +0 -219
- package/docs/internal/TODO-BACKLOG.md +0 -263
- package/docs/internal/TODO-GRAPH.html +0 -715
- package/docs/internal/TODO-GRAPH.html.bak-20260914-v2 +0 -493
- package/docs/internal/TODO-GRAPH.html.bak-20260915-alsfix +0 -710
- package/docs/internal/TODO-GRAPH.html.bak-20260915-p1 +0 -710
- package/docs/internal/TODO-GRAPH.html.bak-20260915-p6a-rev +0 -703
- package/docs/internal/TODO-GRAPH.html.bak-20260915-wshint +0 -710
- package/docs/internal/TODO-GRAPH.html.bak-20260916-batch +0 -715
- package/docs/internal/UPSTREAM-ISSUE-PR-TRIAGE-20260919.md +0 -297
- package/docs/internal/UPSTREAM-ISSUES-3RD-AUDIT-20260920.md +0 -104
- package/docs/internal/WB-FORMAT-CONVENTION.md +0 -112
- package/docs/internal/WB-GRAPH-DECISIONS-20260914.md +0 -71
- package/docs/internal/WB-GRAPH-INTEGRATION-PLAN.md +0 -386
- package/docs/internal/WB-GRAPH-RESEARCH-BRIEF.md +0 -118
- package/docs/internal/WB-GRAPH-RESEARCH-EXTERNAL.md +0 -228
- package/docs/internal/WB-GRAPH-RESEARCH-LOCAL.md +0 -190
- package/docs/internal/reviews/CLAIM-VERIFICATION-20260914.md +0 -56
- package/docs/internal/reviews/PLAN-gpt6astra-round2-20260914.md +0 -787
- package/docs/internal/reviews/REVIEW-gpt6astra-20260914.md +0 -112
- package/docs/internal/reviews/ROUND3-REVIEW-INTEGRATION-20260914.md +0 -230
package/lib/context-host.js
CHANGED
|
@@ -28,6 +28,7 @@ import { createPythonContextSinkPre } from './context-sink-python.js'
|
|
|
28
28
|
import { buildQueryPlan, lexicalSearch, GATE_POLICY_VERSION, LEXICAL_POLICY_VERSION } from './shadow-retrieval.js'
|
|
29
29
|
import { fuseD6Pre } from './semantic-js.js'
|
|
30
30
|
import { buildSourceCatalog, loadCorpusSnapshot, CorpusRegistry, canonicalize } from './m4-corpus.js'
|
|
31
|
+
import { resolveProcedureInjectEnabledPre } from './procedure-switch.js'
|
|
31
32
|
|
|
32
33
|
const MAX_DROPS_RING = 64
|
|
33
34
|
/** 同一 drop 原因的日志限流窗口(ms):窗口内只写首条,其余计数。防会话回放刷屏(2026-09-10)。 */
|
|
@@ -534,7 +535,12 @@ export function createContextHost(opts = {}) {
|
|
|
534
535
|
// 集合不变则缓存命中);不可用回退词法 2-gram。阈值 0.6(e5 相关内容带)。单 offer 出口。
|
|
535
536
|
try {
|
|
536
537
|
const hub = engine._memoryHub
|
|
537
|
-
|
|
538
|
+
// B-2:旧键 procedurePromotionEnabled 是**语义错配的兼容别名**——它的键名与界面文案写的是
|
|
539
|
+
// 「技能固化与晋升」,但它实际控制的是「已激活的技能是否随本轮上下文注入」这道总闸,
|
|
540
|
+
// 与晋升流程本身无关。改由接口契约模块 lib/procedure-switch.js 统一解析:
|
|
541
|
+
// 新键 procedureInjectEnabled 优先,缺省回退旧键,两键都缺省按开启处理;
|
|
542
|
+
// 老用户显式关掉过总闸的,行为不翻面。此处只替换开关解析口径,不新增任何前置条件。
|
|
543
|
+
const skillEnabled = engine.config.memoryHubEnabled === true && resolveProcedureInjectEnabledPre(engine.config)
|
|
538
544
|
if (hub && skillEnabled && hub.stores && hub.stores.procedures) {
|
|
539
545
|
const actives = hub.stores.procedures.activeProcedures()
|
|
540
546
|
let hit = null
|
package/lib/episodic-store.js
CHANGED
|
@@ -121,15 +121,43 @@ export function createEpisodicStorePre(opts = {}) {
|
|
|
121
121
|
return raw
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
+
/**
|
|
125
|
+
* ★ P2-2 修复(2026-09-21):查询返回**深一层副本**。
|
|
126
|
+
* 旧实现 `{ ...e }` 是浅拷贝 ⇒ `actions/entities/unresolved/provenance` 四个数组
|
|
127
|
+
* 与库内对象**同引用**,调用方 push 会直接改脏已巩固 episode(绕过 persist 与统计)。
|
|
128
|
+
*/
|
|
129
|
+
function cloneEpisode(e) {
|
|
130
|
+
if (!e) return e
|
|
131
|
+
return {
|
|
132
|
+
...e,
|
|
133
|
+
actions: Array.isArray(e.actions) ? [...e.actions] : e.actions,
|
|
134
|
+
entities: Array.isArray(e.entities) ? [...e.entities] : e.entities,
|
|
135
|
+
unresolved: Array.isArray(e.unresolved) ? [...e.unresolved] : e.unresolved,
|
|
136
|
+
provenance: Array.isArray(e.provenance) ? [...e.provenance] : e.provenance,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
124
140
|
// ---- 段追加(会话进行中实时累积) ----
|
|
125
141
|
function append(seg) {
|
|
126
142
|
if (disposed) return { ok: false, reason: 'disposed' }
|
|
127
143
|
const v = validateEpisodeSegmentPre(seg)
|
|
128
144
|
if (!v.ok) return { ok: false, reason: v.reason }
|
|
129
145
|
const s = v.segment
|
|
146
|
+
const incomingRef = String(s.sessionRef || 'unknown')
|
|
147
|
+
// ★ A-7 修复(2026-09-21):**按 sessionRef 隔离**。
|
|
148
|
+
// 旧实现只在 `!current` 时取一次 sessionRef ⇒ 后续不同会话的段被并进同一个 episode:
|
|
149
|
+
// 实测 sessionRef=session-A 但 intent 含 session-B 内容、provenance=["seg:1","seg:1"]。
|
|
150
|
+
// 后果:procedure 晋升依赖的 distinctSessions 被系统性低估 ⇒ 技能永远卡在
|
|
151
|
+
// `diversity-below-3`(跨会话证据明明够,读数却恒为 1)。
|
|
152
|
+
// 现改为:检测到会话切换时,**先巩固当前缓冲**(把上一会话的段固化成一个独立 episode),
|
|
153
|
+
// 再为新的 sessionRef 开一个干净的 current。consolidate 失败(如 too-short)不阻断本段,
|
|
154
|
+
// 其内部已负责把 current 置 null,后续照常新建。
|
|
155
|
+
if (current && current.sessionRef !== incomingRef) {
|
|
156
|
+
consolidate()
|
|
157
|
+
}
|
|
130
158
|
if (!current) {
|
|
131
159
|
current = {
|
|
132
|
-
sessionRef:
|
|
160
|
+
sessionRef: incomingRef,
|
|
133
161
|
startedAt: nowFn(),
|
|
134
162
|
segments: [],
|
|
135
163
|
userTexts: [], assistantTexts: [],
|
|
@@ -156,7 +184,13 @@ export function createEpisodicStorePre(opts = {}) {
|
|
|
156
184
|
if (current.assistantTexts.length) current.assistantTexts.shift()
|
|
157
185
|
}
|
|
158
186
|
stats.segmentsAppended++
|
|
159
|
-
|
|
187
|
+
// ★ P2-11 修复(2026-09-21):append 此前**完全不落盘** —— 段只留在内存,
|
|
188
|
+
// 进程重启(崩溃/宿主重载)后未巩固的会话缓冲整段丢失,而调用方看到 ok:true。
|
|
189
|
+
// 现在按段落盘(与 fact-store 各写路径一致)。落盘结果一并透传(A-8 同款纪律)。
|
|
190
|
+
const pr = persist()
|
|
191
|
+
return pr.ok
|
|
192
|
+
? { ok: true, segments: current.segments.length, persisted: true }
|
|
193
|
+
: { ok: true, segments: current.segments.length, persisted: false, persistError: pr.error }
|
|
160
194
|
}
|
|
161
195
|
|
|
162
196
|
/** 从累积文本提取 intent(2026-08-28 提纯;2026-09-16 issue#30 改为共享清洗器)。
|
|
@@ -232,13 +266,24 @@ export function createEpisodicStorePre(opts = {}) {
|
|
|
232
266
|
unresolved: extractUnresolved(current),
|
|
233
267
|
outcome,
|
|
234
268
|
success: outcome === 'success',
|
|
235
|
-
|
|
269
|
+
// ★ A-7 修复(2026-09-21):provenance 改为 **`sessionRef:eventSeq`**。
|
|
270
|
+
// 旧实现只用 `'seg:' + eventSeq` ⇒ 不同会话里 eventSeq 从 1 重新计数时
|
|
271
|
+
// 会产出完全相同的串(实测 ["seg:1","seg:1"]),既无法溯源到会话,
|
|
272
|
+
// 也让"同一段被重复计入"与"两段恰好同号"在审计面上不可区分。
|
|
273
|
+
provenance: current.segments.map((s) => String(current.sessionRef) + ':' + String(s.eventSeq)),
|
|
236
274
|
startedAt: current.startedAt,
|
|
237
275
|
consolidatedAt: nowFn(),
|
|
238
276
|
}
|
|
239
277
|
const v = validateEpisodePre(ep)
|
|
240
278
|
if (!v.ok) { current = null; return { ok: false, reason: 'invalid:' + v.reason } }
|
|
241
|
-
|
|
279
|
+
// ★ P2-11 修复(2026-09-21):巩固入店前按 **episodeId 去重**。
|
|
280
|
+
// episodeId = hash(sessionRef, startedAt) ⇒ 同一会话在同一时间戳被重复巩固
|
|
281
|
+
// (或 restore/import 已带入同 id 记录)会产出**同一主键的第二条**,
|
|
282
|
+
// 下游 query/statsFor 会把一次会话数成两次 ⇒ distinctSessions/success 读数虚高。
|
|
283
|
+
// 去重策略:**后到者胜**(原地替换,不打乱既有顺序),与 fact-store restore 一致。
|
|
284
|
+
const dupAt = episodes.findIndex((x) => x.episodeId === v.episode.episodeId)
|
|
285
|
+
if (dupAt >= 0) episodes[dupAt] = v.episode
|
|
286
|
+
else episodes.push(v.episode)
|
|
242
287
|
// 保留策略: 超 retention 淘汰最旧
|
|
243
288
|
if (episodes.length > cfg.retention) {
|
|
244
289
|
episodes = episodes.slice(-cfg.retention)
|
|
@@ -246,8 +291,10 @@ export function createEpisodicStorePre(opts = {}) {
|
|
|
246
291
|
}
|
|
247
292
|
current = null
|
|
248
293
|
stats.consolidated++
|
|
249
|
-
|
|
250
|
-
return
|
|
294
|
+
const pr = persist()
|
|
295
|
+
return pr.ok
|
|
296
|
+
? { ok: true, episode: v.episode, persisted: true }
|
|
297
|
+
: { ok: true, episode: v.episode, persisted: false, persistError: pr.error }
|
|
251
298
|
}
|
|
252
299
|
|
|
253
300
|
/** 会话中途强制巩固(跨天续接/会话切换时调用)。 */
|
|
@@ -264,14 +311,14 @@ export function createEpisodicStorePre(opts = {}) {
|
|
|
264
311
|
(q.sessionRef === undefined || e.sessionRef === q.sessionRef) &&
|
|
265
312
|
(q.outcome === undefined || e.outcome === q.outcome) &&
|
|
266
313
|
(q.success === undefined || e.success === q.success))
|
|
267
|
-
.map(
|
|
314
|
+
.map(cloneEpisode)
|
|
268
315
|
}
|
|
269
316
|
function recent(n = 10) {
|
|
270
|
-
return episodes.slice(-n).map(
|
|
317
|
+
return episodes.slice(-n).map(cloneEpisode)
|
|
271
318
|
}
|
|
272
319
|
function get(episodeId) {
|
|
273
320
|
const e = episodes.find((x) => x.episodeId === episodeId)
|
|
274
|
-
return e ?
|
|
321
|
+
return e ? cloneEpisode(e) : null
|
|
275
322
|
}
|
|
276
323
|
|
|
277
324
|
// ---- M-04 挂钩: 供 Procedure 晋升用的事故/成功统计 ----
|
|
@@ -290,10 +337,12 @@ export function createEpisodicStorePre(opts = {}) {
|
|
|
290
337
|
return {
|
|
291
338
|
schemaVersion: 1, namespace: 'dsh-auto-memory', policyVersion: EPISODIC_POLICY_VERSION,
|
|
292
339
|
savedAt: nowFn(),
|
|
293
|
-
|
|
340
|
+
// ★ P2-2 修复:快照数组字段同样做副本(调用方改快照不得改脏库内 episode)
|
|
341
|
+
episodes: episodes.map(cloneEpisode),
|
|
294
342
|
current: current ? {
|
|
295
343
|
sessionRef: current.sessionRef, startedAt: current.startedAt,
|
|
296
|
-
segments: current.segments
|
|
344
|
+
segments: current.segments.map((s) => ({ ...s })),
|
|
345
|
+
userTexts: [...current.userTexts], assistantTexts: [...current.assistantTexts],
|
|
297
346
|
} : null,
|
|
298
347
|
}
|
|
299
348
|
}
|
|
@@ -344,22 +393,47 @@ export function createEpisodicStorePre(opts = {}) {
|
|
|
344
393
|
}
|
|
345
394
|
function clear() {
|
|
346
395
|
episodes = []; current = null
|
|
347
|
-
|
|
348
|
-
|
|
396
|
+
// ★ A-8:io.clear() 失败不得静默(残留快照会在下次 load 时"复活"已清空的 episode)
|
|
397
|
+
let cleared = true, clearError
|
|
398
|
+
try { io.clear() } catch (e) {
|
|
399
|
+
cleared = false
|
|
400
|
+
clearError = (e && e.message) ? String(e.message) : String(e)
|
|
401
|
+
}
|
|
402
|
+
return clearError !== undefined ? { ok: true, cleared, error: clearError } : { ok: true, cleared }
|
|
349
403
|
}
|
|
350
404
|
function dispose(reason) {
|
|
351
|
-
if (disposed) return
|
|
405
|
+
if (disposed) return { ok: true, persisted: true, alreadyDisposed: true }
|
|
352
406
|
disposed = true
|
|
353
|
-
|
|
407
|
+
// ★ A-8:dispose 是最后一次落盘机会,失败必须外显
|
|
408
|
+
const r = persist()
|
|
409
|
+
return { ok: r.ok, persisted: r.ok, ...(r.error ? { error: r.error } : {}) }
|
|
354
410
|
}
|
|
355
411
|
|
|
412
|
+
/**
|
|
413
|
+
* ★ A-8 修复(2026-09-21):旧实现 `try { io.save(snapshot()) } catch (_) {}` **吞掉落盘失败** ——
|
|
414
|
+
* 调用方看到 ok:true、宿主继续推进状态,但磁盘没写上,该状态**永不重写**(静默数据丢失)。
|
|
415
|
+
* 现改为:① 返回结构化结果 `{ok, persisted}`(失败时附 error 文案); ② 失败记进模块内可读状态
|
|
416
|
+
* `lastPersistError`(经 `getLastPersistError()` 暴露)。不引入任何新 import。
|
|
417
|
+
* @returns {{ok:boolean, persisted:boolean, error?:string}}
|
|
418
|
+
*/
|
|
419
|
+
let lastPersistError = null
|
|
356
420
|
function persist() {
|
|
357
|
-
try {
|
|
421
|
+
try {
|
|
422
|
+
io.save(snapshot())
|
|
423
|
+
lastPersistError = null
|
|
424
|
+
return { ok: true, persisted: true }
|
|
425
|
+
} catch (e) {
|
|
426
|
+
const msg = (e && e.message) ? String(e.message) : String(e)
|
|
427
|
+
lastPersistError = msg
|
|
428
|
+
return { ok: false, persisted: false, error: msg }
|
|
429
|
+
}
|
|
358
430
|
}
|
|
359
431
|
|
|
360
432
|
return {
|
|
361
433
|
append, consolidate, flush, query, recent, get, statsFor,
|
|
362
434
|
snapshot, restore, importEpisodes, clear, dispose,
|
|
435
|
+
/** ★ A-8:最近一次落盘失败(字符串)或 null —— 供宿主诊断"写盘失败但流程继续"的静默缺口。 */
|
|
436
|
+
getLastPersistError: () => lastPersistError,
|
|
363
437
|
getStats: () => ({ ...stats }),
|
|
364
438
|
get size() { return episodes.length },
|
|
365
439
|
get hasCurrent() { return !!current },
|