@a9i5k4/dsh-auto-memory 3.0.1 → 3.1.1
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 +92 -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 +1376 -255
- package/lib/config-io.js +59 -6
- package/lib/context-host.js +17 -2
- package/lib/episodic-store.js +90 -16
- package/lib/evidence-store.js +27 -1
- package/lib/fact-store.js +463 -41
- package/lib/hub-io.js +217 -0
- package/lib/index.js +904 -87
- package/lib/intent-clean-safe.js +1 -1
- package/lib/jsonl-tail-cursor.js +75 -0
- package/lib/m7-index-sync-host.js +4 -6
- package/lib/memory-hub.js +62 -7
- package/lib/migrate-pack.js +351 -0
- package/lib/note-status.js +9 -1
- package/lib/procedure-store.js +252 -31
- package/lib/procedure-switch.js +38 -0
- package/lib/python-setup.js +109 -27
- package/lib/python-sidecar-client.js +285 -8
- package/lib/recall-stats.js +242 -0
- package/lib/rules-layer.js +106 -15
- package/lib/semantic-js.js +25 -7
- package/lib/shadow-host.js +41 -2
- 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/config-io.js
CHANGED
|
@@ -27,12 +27,34 @@
|
|
|
27
27
|
* - CRLF、无 BOM。
|
|
28
28
|
*/
|
|
29
29
|
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs'
|
|
30
|
-
import { writeFile } from 'node:fs/promises'
|
|
30
|
+
import { rename, writeFile } from 'node:fs/promises'
|
|
31
|
+
import { setTimeout as sleepDelayPre } from 'node:timers/promises'
|
|
31
32
|
import path from 'node:path'
|
|
32
33
|
|
|
33
34
|
/** 临时文件后缀。与既有 hub 落盘的 `.tmp` 同族,便于排查时一眼认出。 */
|
|
34
35
|
export const ATOMIC_TMP_SUFFIX_V1 = '.tmp'
|
|
35
36
|
|
|
37
|
+
/**
|
|
38
|
+
* ★2026-09-22(并发写修复):tmp 路径的**唯一段** = pid + 进程内自增序号。
|
|
39
|
+
*
|
|
40
|
+
* 病症:tmp 名原先**只由目标路径决定**(`file + '.tmp'`)⇒ 对同一目标并发保存时,
|
|
41
|
+
* 先完成者把 tmp 改名走,后到者的 rename 找不到源文件而抛 **ENOENT**(非瞬时错误、
|
|
42
|
+
* 按纪律不得重试)⇒ 后一次保存静默失败(只回 `{ok:false}`,界面看起来"改了没生效")。
|
|
43
|
+
* 实测复现:对同一目标并发 5 次 `writeTextAtomicPre`,部分调用返回
|
|
44
|
+
* `{ok:false, error:'ENOENT … rename … .tmp -> …'}`(见 `tests/smoke/smoke-test-p3-batchb-guards-pre.mjs` P3B-2d)。
|
|
45
|
+
*
|
|
46
|
+
* 处方:每次调用生成一个独立的 tmp 名 —— 同进程并发各写各的,跨进程并发靠 pid 区分;
|
|
47
|
+
* 最后一次 rename 胜出,**原子性不变量不变**(目标任意时刻仍是旧完整版或新完整版)。
|
|
48
|
+
*
|
|
49
|
+
* 边界:仍落在**同目录**且保持 `.tmp` 前缀(`rename` 必须同文件系统);失败路径照旧
|
|
50
|
+
* `rmSync` 清理。唯一代价是**进程崩溃**时会在目标目录留下一个 `.tmp-<pid>-<n>`
|
|
51
|
+
* (可识别、可手工清理;不自动扫描删除,避免误删他人正在写的 tmp)。
|
|
52
|
+
*/
|
|
53
|
+
let _tmpSeqPre = 0
|
|
54
|
+
function tmpUniqueSuffixPre() {
|
|
55
|
+
return '-' + process.pid + '-' + (++_tmpSeqPre)
|
|
56
|
+
}
|
|
57
|
+
|
|
36
58
|
/** 损坏留存的默认后缀前缀。 */
|
|
37
59
|
export const CORRUPT_QUARANTINE_PREFIX_V1 = '.corrupt-'
|
|
38
60
|
|
|
@@ -70,7 +92,7 @@ export function quarantineFilePreSync(file, opts = {}) {
|
|
|
70
92
|
* @returns {{ok: boolean, path?: string, error?: string}} 结构化结果(fail-soft,不抛)。
|
|
71
93
|
*/
|
|
72
94
|
export function writeTextAtomicPreSync(file, text) {
|
|
73
|
-
const tmp = file + ATOMIC_TMP_SUFFIX_V1
|
|
95
|
+
const tmp = file + ATOMIC_TMP_SUFFIX_V1 + tmpUniqueSuffixPre()
|
|
74
96
|
try {
|
|
75
97
|
mkdirSync(path.dirname(file), { recursive: true })
|
|
76
98
|
writeFileSync(tmp, String(text), 'utf8')
|
|
@@ -83,18 +105,49 @@ export function writeTextAtomicPreSync(file, text) {
|
|
|
83
105
|
}
|
|
84
106
|
}
|
|
85
107
|
|
|
108
|
+
/**
|
|
109
|
+
* **有界退避 rename**(★P3-12,2026-09-22)。
|
|
110
|
+
*
|
|
111
|
+
* 病症:异步原子写原先走裸 `renameSync` —— Windows 上目标文件被别的进程持有句柄时会抛
|
|
112
|
+
* EPERM/EACCES/EBUSY,于是**整次设置保存失败**(用户侧表现为"改了设置没生效")。
|
|
113
|
+
* 处方:仅对这三个**瞬时**错误码退避重试;首项延迟为 0 ⇒ **正常路径零延迟变化**。
|
|
114
|
+
*
|
|
115
|
+
* ⚠️ **为什么不 import `fs-retry.js`**:本模块的**零运行时依赖纪律**(见文件头)
|
|
116
|
+
* 被 `tests/smoke/smoke-test-t8-configio-pre.mjs` 的 T8c-6 钉死 —— 配置 I/O 层不得因为
|
|
117
|
+
* 任何兄弟模块的加载期故障而失效。故此处**内联**同一套退避刻度(只用 node: 内置),
|
|
118
|
+
* 语义与 `fs-retry.js` 的 `retryRename` 保持一致。
|
|
119
|
+
*/
|
|
120
|
+
const RENAME_RETRY_DELAYS_V1 = Object.freeze([0, 50, 150, 400, 1000])
|
|
121
|
+
const TRANSIENT_RENAME_CODES_V1 = new Set(['EPERM', 'EACCES', 'EBUSY'])
|
|
122
|
+
async function renameBoundedRetryPre(from, to) {
|
|
123
|
+
const last = RENAME_RETRY_DELAYS_V1.length - 1
|
|
124
|
+
for (let attempt = 0; attempt <= last; attempt++) {
|
|
125
|
+
const wait = RENAME_RETRY_DELAYS_V1[attempt]
|
|
126
|
+
if (wait) await sleepDelayPre(wait)
|
|
127
|
+
try {
|
|
128
|
+
await rename(from, to)
|
|
129
|
+
return
|
|
130
|
+
} catch (e) {
|
|
131
|
+
const transient = TRANSIENT_RENAME_CODES_V1.has(e && e.code)
|
|
132
|
+
if (!transient || attempt === last) throw e
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
86
137
|
/**
|
|
87
138
|
* **异步原子写**:tmp → rename。语义与同步版一致。
|
|
88
139
|
*
|
|
89
|
-
* 只把**可能较大**的正文写入交给异步 IO
|
|
90
|
-
*
|
|
140
|
+
* 只把**可能较大**的正文写入交给异步 IO。
|
|
141
|
+
*
|
|
142
|
+
* ★P3-12(2026-09-22):rename 走 `renameBoundedRetryPre`(有界退避;首项 0 ⇒ 正常路径零延迟变化)。
|
|
143
|
+
* 同步版 `writeTextAtomicPreSync` 的契约是同步,保持 `renameSync` 不动。
|
|
91
144
|
*/
|
|
92
145
|
export async function writeTextAtomicPre(file, text) {
|
|
93
|
-
const tmp = file + ATOMIC_TMP_SUFFIX_V1
|
|
146
|
+
const tmp = file + ATOMIC_TMP_SUFFIX_V1 + tmpUniqueSuffixPre()
|
|
94
147
|
try {
|
|
95
148
|
mkdirSync(path.dirname(file), { recursive: true })
|
|
96
149
|
await writeFile(tmp, String(text), 'utf8')
|
|
97
|
-
|
|
150
|
+
await renameBoundedRetryPre(tmp, file)
|
|
98
151
|
return { ok: true, path: file }
|
|
99
152
|
} catch (e) {
|
|
100
153
|
try { rmSync(tmp, { force: true }) } catch (_) {}
|
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)。 */
|
|
@@ -163,7 +164,11 @@ export function createContextHost(opts = {}) {
|
|
|
163
164
|
return bridge
|
|
164
165
|
}
|
|
165
166
|
function storeFor() {
|
|
166
|
-
if (!store) store = new EvidenceEventStore({
|
|
167
|
+
if (!store) store = new EvidenceEventStore({
|
|
168
|
+
root: path.join(dshHome(), 'memory', 'evidence'),
|
|
169
|
+
// ★P3-9(组2,2026-09-22):注入统一节流诊断通道;不注入时 store 内回落 console.error。
|
|
170
|
+
onDiag: (key, msg) => { try { diagCtx(msg) } catch (_) {} },
|
|
171
|
+
})
|
|
167
172
|
return store
|
|
168
173
|
}
|
|
169
174
|
function evidenceRootExists() {
|
|
@@ -534,7 +539,12 @@ export function createContextHost(opts = {}) {
|
|
|
534
539
|
// 集合不变则缓存命中);不可用回退词法 2-gram。阈值 0.6(e5 相关内容带)。单 offer 出口。
|
|
535
540
|
try {
|
|
536
541
|
const hub = engine._memoryHub
|
|
537
|
-
|
|
542
|
+
// B-2:旧键 procedurePromotionEnabled 是**语义错配的兼容别名**——它的键名与界面文案写的是
|
|
543
|
+
// 「技能固化与晋升」,但它实际控制的是「已激活的技能是否随本轮上下文注入」这道总闸,
|
|
544
|
+
// 与晋升流程本身无关。改由接口契约模块 lib/procedure-switch.js 统一解析:
|
|
545
|
+
// 新键 procedureInjectEnabled 优先,缺省回退旧键,两键都缺省按开启处理;
|
|
546
|
+
// 老用户显式关掉过总闸的,行为不翻面。此处只替换开关解析口径,不新增任何前置条件。
|
|
547
|
+
const skillEnabled = engine.config.memoryHubEnabled === true && resolveProcedureInjectEnabledPre(engine.config)
|
|
538
548
|
if (hub && skillEnabled && hub.stores && hub.stores.procedures) {
|
|
539
549
|
const actives = hub.stores.procedures.activeProcedures()
|
|
540
550
|
let hit = null
|
|
@@ -813,6 +823,11 @@ export function createContextHost(opts = {}) {
|
|
|
813
823
|
const st = states.get(runtime)
|
|
814
824
|
if (st) st.disposed = true
|
|
815
825
|
states.delete(runtime)
|
|
826
|
+
// ★P3-3(a)(2026-09-22):pathsByKey 原先是「模块生命周期内永不释放」的集合——
|
|
827
|
+
// 写入点只有 capturePaths(每会话一条),全文件零 delete/clear;且 disposeAll 走本函数,
|
|
828
|
+
// 连全量退出路径也清不掉 ⇒ findProvenance 会在已释放的 runtime 上命中陈旧快照。
|
|
829
|
+
// 键口径与 :316/:482/:683 三处读点一致(String(runtime.key || ''))。
|
|
830
|
+
pathsByKey.delete(String(runtime.key || ''))
|
|
816
831
|
}
|
|
817
832
|
function disposeAll(reason) {
|
|
818
833
|
for (const pair of collectStates()) disposeRuntime(pair.runtime)
|
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 },
|
package/lib/evidence-store.js
CHANGED
|
@@ -95,6 +95,23 @@ export function parseDurableEventLine(line) {
|
|
|
95
95
|
|
|
96
96
|
// ========== Store(appen-only JSONL,engine 级串行链) ==========
|
|
97
97
|
|
|
98
|
+
/**
|
|
99
|
+
* ★P3-9(组2,2026-09-22):诊断**节流**表。
|
|
100
|
+
* 原先 `_writeLine` 每次写失败都直接 console.error ⇒ 持续失败(磁盘满/权限错)时刷屏。
|
|
101
|
+
* 表放**模块级**而非实例级:同进程多实例共享 ⇒ 不会各自持一份而双写。
|
|
102
|
+
* 语义同 index.js 的 diagThrottled:首条立即透出,窗口内重复压掉。
|
|
103
|
+
*/
|
|
104
|
+
const DIAG_THROTTLE_MS_V1 = 300000
|
|
105
|
+
const _diagAt = new Map()
|
|
106
|
+
function shouldReportDiag(key) {
|
|
107
|
+
try {
|
|
108
|
+
const now = Date.now()
|
|
109
|
+
if (now - (Number(_diagAt.get(key)) || 0) < DIAG_THROTTLE_MS_V1) return false
|
|
110
|
+
_diagAt.set(key, now)
|
|
111
|
+
} catch (_) { /* 计时失败不阻断上报 */ }
|
|
112
|
+
return true
|
|
113
|
+
}
|
|
114
|
+
|
|
98
115
|
export class EvidenceEventStore {
|
|
99
116
|
constructor(opts = {}) {
|
|
100
117
|
this.root = opts.root || null
|
|
@@ -103,6 +120,15 @@ export class EvidenceEventStore {
|
|
|
103
120
|
this._appended = new BoundedIdSet(EVIDENCE_STORE_POLICY_V1.appendedIdCache)
|
|
104
121
|
this._swept = false
|
|
105
122
|
this.stats = { appended: 0, duplicates: 0, oversize: 0, invalid: 0, writeFailed: 0, sweptFiles: 0, lastWriteError: null }
|
|
123
|
+
// ★P3-9(组2):可注入诊断通道(宿主不注入时 reportDiag 回落 console.error,行为向后兼容)。
|
|
124
|
+
this._onDiag = typeof opts.onDiag === 'function' ? opts.onDiag : null
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** ★P3-9(组2):节流上报。首条立即透出,窗口内重复压掉。 */
|
|
128
|
+
reportDiag(key, msg) {
|
|
129
|
+
if (!shouldReportDiag(key)) return
|
|
130
|
+
if (this._onDiag) { try { this._onDiag(key, msg); return } catch (_) { /* 回落 */ } }
|
|
131
|
+
try { console.error(msg) } catch (_) {}
|
|
106
132
|
}
|
|
107
133
|
|
|
108
134
|
/** §9 幂等:同 evidenceId 进程内只落盘一次(seed replay 与 live 双喂安全网;磁盘侧靠 rebuild 去重兜底)。 */
|
|
@@ -140,7 +166,7 @@ export class EvidenceEventStore {
|
|
|
140
166
|
} catch (e) {
|
|
141
167
|
this.stats.writeFailed++
|
|
142
168
|
this.stats.lastWriteError = String(e && e.message ? e.message : e)
|
|
143
|
-
try {
|
|
169
|
+
try { this.reportDiag('evidence:write-failed', '[evidence-store] write-failed: ' + this.stats.lastWriteError) } catch (_) {}
|
|
144
170
|
return false
|
|
145
171
|
}
|
|
146
172
|
}
|