@a9i5k4/dsh-auto-memory 3.1.4 → 3.1.6
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/docs/CONTINUITY-FLOW.md +6 -6
- package/docs/CONTRIBUTORS.html +966 -471
- package/docs/HANDBOOK.md +54 -53
- package/docs/M-CM-PLAN.md +1 -1
- package/docs/M-CM-STATE.md +1 -1
- package/docs/M-CM7-HANDOFF-LAYERED-RETRIEVAL.md +1 -1
- package/docs/M3B-CONTRACT.md +7 -7
- package/docs/M6-CONTRACT.md +1 -1
- package/docs/M7-AUTONOMOUS-STATE.md +1 -1
- package/docs/M7-TASKSET-DISPATCH.md +1 -1
- package/docs/PROMPT-PACK-LAYERED-RECALL.md +3 -3
- package/docs/PROMPT-SET-STRICT.md +3 -3
- package/docs/RELEASE-GO-NOGO.md +1 -1
- package/docs/STATUS-BOARD.md +1 -1
- package/docs/USER-GUIDE.en.md +21 -21
- package/docs/USER-GUIDE.zh-CN.md +21 -21
- package/docs/WHITEPAPER.md +1 -1
- package/docs/landing/index.html +1178 -1624
- package/docs/prompts/FIX-AGENT-P12-FULL-REGRESSION.md +1 -1
- package/docs/prompts/LIVE-VERIFY-ZCODE.md +2 -2
- package/docs/prompts/P2-semantic-recall.md +2 -2
- package/docs/prompts/P7-write-fix.md +1 -1
- package/docs/prompts/ZCODE-DROPIN.md +1 -1
- package/lib/acceptance.js +6 -6
- package/lib/activation-host.js +15 -14
- package/lib/activation-inbox-state.js +4 -4
- package/lib/activation-inbox.js +77 -47
- package/lib/board-mode.js +3 -3
- package/lib/client.js +206 -34
- package/lib/config-io.js +12 -12
- package/lib/context-bridge.js +22 -22
- package/lib/context-host.js +24 -23
- package/lib/datadir.js +95 -0
- package/lib/degrade.js +21 -21
- package/lib/dsh-home.js +8 -8
- package/lib/engine-identity.js +12 -12
- package/lib/engine-switch.js +3 -3
- package/lib/episodic-store.js +13 -13
- package/lib/evidence-agg.js +4 -4
- package/lib/evidence-store.js +12 -12
- package/lib/fact-store.js +47 -47
- package/lib/handoff-anchor.js +4 -4
- package/lib/hub-io.js +356 -3
- package/lib/index-sync.js +5 -5
- package/lib/index.js +381 -236
- package/lib/intent-clean-safe.js +3 -3
- package/lib/intent-clean.js +1 -1
- package/lib/l0-extract.js +15 -15
- package/lib/l0-index-sync.js +8 -8
- package/lib/l0-index.js +11 -11
- package/lib/ledger-criteria.js +8 -8
- package/lib/m4-corpus.js +1 -1
- package/lib/m7-index-sync-host.js +2 -2
- package/lib/m7-wire.js +24 -24
- package/lib/memory-anchor.js +1 -1
- package/lib/memory-envelope.js +15 -15
- package/lib/memory-hub.js +95 -50
- package/lib/memory-importance.js +7 -7
- package/lib/memory-mutation.js +5 -5
- package/lib/note-status-apply.js +2 -2
- package/lib/note-status.js +7 -7
- package/lib/policies/activation_policy_v2.json +2 -2
- package/lib/policies/recall_intent_lr_v1.json +1 -1
- package/lib/procedure-observation.js +2 -2
- package/lib/procedure-store.js +117 -28
- package/lib/python-setup.js +6 -5
- package/lib/python-sidecar-client.js +26 -26
- package/lib/recall-fusion.js +12 -12
- package/lib/recall-stats.js +36 -1
- package/lib/rerank-host.js +11 -11
- package/lib/rules-edit.js +9 -9
- package/lib/rules-layer.js +26 -26
- package/lib/semantic-decide.js +7 -7
- package/lib/semantic-js.js +13 -13
- package/lib/shadow-host.js +7 -6
- package/lib/shadow-retrieval.js +40 -40
- package/lib/skill-export-host.js +7 -7
- package/lib/skill-export.js +6 -6
- package/lib/state-commit.js +10 -10
- package/lib/storage-manage.js +6 -6
- package/lib/tier-layer-inject.js +28 -28
- package/lib/tier0-catalog.js +4 -4
- package/lib/wb-contract.js +45 -45
- package/lib/wb-sidecar.js +11 -11
- package/package.json +3 -4
- package/python/m7_activation_features_v2.py +7 -7
- package/python/m7_embedding_v1.py +12 -12
- package/python/policies/activation_policy_v2.json +2 -2
- package/python/policies/decision-record-activation-v2-delta-exp-override-20260824.json +1 -1
- package/python/policies/decision-record-reasoning-kind-admission-20260826.json +2 -2
- package/python/policies/decision-record-stale-gate-per-candidate-20260825.json +2 -2
- package/python/policies/recall_intent_lr_v1.json +1 -1
- package/python/verify_policy_artifact.py +3 -3
- package/python/worker_semantic_v1.py +18 -18
- package/python/worker_v1.py +18 -18
package/lib/procedure-store.js
CHANGED
|
@@ -61,28 +61,43 @@ import { createHash } from 'node:crypto'
|
|
|
61
61
|
|
|
62
62
|
// ========== 冻结常量 ==========
|
|
63
63
|
|
|
64
|
-
export const PROCEDURE_POLICY_VERSION = '
|
|
65
|
-
export const PROCEDURE_ID_PREFIX = '
|
|
66
|
-
export const PROCEDURE_ID_RE = /^
|
|
64
|
+
export const PROCEDURE_POLICY_VERSION = 'procedure_store_pre_v1'
|
|
65
|
+
export const PROCEDURE_ID_PREFIX = 'proc_pre_'
|
|
66
|
+
export const PROCEDURE_ID_RE = /^proc_pre_[0-9a-f]{32}$/
|
|
67
67
|
|
|
68
68
|
/** 阶段枚举(M-04 元代码)。P2-3:移除死状态 'candidate' —— 全仓无写入点,
|
|
69
69
|
* observe 只产 observed、promote 直接 observed→validated(两态跳变)。 */
|
|
70
|
-
export const
|
|
70
|
+
export const PROCEDURE_STAGES_PRE_V1 = Object.freeze(['observed', 'validated', 'active', 'deprecated'])
|
|
71
71
|
|
|
72
72
|
/** 自动归档老化(Hermes curator 借鉴,2026-08-28):active 技能 lastUsedAt 超过
|
|
73
|
-
*
|
|
73
|
+
* PROCEDURE_ARCHIVE_AFTER_DAYS_PRE_V1 天未使用且未 pinned → 自动 deprecated
|
|
74
74
|
* (reason=auto-archive-inactive;deprecated 可恢复,永不物理删除)。
|
|
75
75
|
* observed/validated 不老化——它们由晋升门槛时间控(P2-3 后已无 candidate 态)。 */
|
|
76
|
-
export const
|
|
76
|
+
export const PROCEDURE_ARCHIVE_AFTER_DAYS_PRE_V1 = 90
|
|
77
77
|
|
|
78
78
|
/** 风险等级枚举。 */
|
|
79
|
-
export const
|
|
79
|
+
export const PROCEDURE_RISKS_PRE_V1 = Object.freeze(['low', 'medium', 'high'])
|
|
80
80
|
|
|
81
81
|
/** 激活等级(M6 六级契约)。 */
|
|
82
|
-
export const
|
|
82
|
+
export const PROCEDURE_LEVELS_PRE_V1 = Object.freeze(['index', 'hint', 'excerpt', 'checklist', 'resource', 'full'])
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* ★M8-B(2026-09-23 用户拍板):记忆库作用域枚举 —— 工作区库 / 通用库。
|
|
86
|
+
*
|
|
87
|
+
* 物理落点由 IO 层按 `scope` 分派(本模块只负责**携带与校验**该字段,不碰路径):
|
|
88
|
+
* · `workspace` → `<workspaces>/<wsKey>/hub-pre/procedures.json`(工作区库,新增)
|
|
89
|
+
* · `global` → `<dshHome>/memory/hub-pre/procedures.json`(通用库,既有 55 条所在)
|
|
90
|
+
*
|
|
91
|
+
* **缺省语义**:字段缺失一律按 `global` 解释 —— 老数据本就全在通用库里,按此缺省即可零迁移;
|
|
92
|
+
* 要进工作区库必须**显式声明**,否则 IO 层 fail-soft 落通用库(绝不丢条目)。
|
|
93
|
+
*/
|
|
94
|
+
export const PROCEDURE_SCOPES_PRE_V1 = Object.freeze(['workspace', 'global'])
|
|
95
|
+
|
|
96
|
+
/** 缺省作用域常量(供 IO 层与调用方共用,避免各写一份字面量)。 */
|
|
97
|
+
export const PROCEDURE_DEFAULT_SCOPE_PRE_V1 = 'global'
|
|
83
98
|
|
|
84
99
|
/** 晋升门槛(可调参数,默认值来自 M-04 元代码): 跨会话多样性≥3, 成功≥2。 */
|
|
85
|
-
export const
|
|
100
|
+
export const PROCEDURE_DEFAULT_GATES_PRE_V1 = Object.freeze({
|
|
86
101
|
minSessionDiversity: 3,
|
|
87
102
|
minSuccessCount: 2,
|
|
88
103
|
maxCorrectionRate: 0.3, // correction 占总证据比例上限
|
|
@@ -91,18 +106,18 @@ export const PROCEDURE_DEFAULT_GATES_V1 = Object.freeze({
|
|
|
91
106
|
})
|
|
92
107
|
|
|
93
108
|
/** 一次成功或三次重复都不足以证明可靠 → 需要的最小成功数(元代码铁律)。 */
|
|
94
|
-
export const
|
|
109
|
+
export const PROCEDURE_MIN_SUCCESS_PRE_V1 = 2
|
|
95
110
|
|
|
96
111
|
/** active 后可注入的默认 level(渐进激活;高风险降级为 hint)。 */
|
|
97
|
-
export const
|
|
112
|
+
export const PROCEDURE_ACTIVE_LEVEL_PRE_V1 = 'checklist'
|
|
98
113
|
|
|
99
114
|
/** procedure 校验(固化/读回)。 */
|
|
100
115
|
export function validateProcedurePre(p) {
|
|
101
116
|
const q = []
|
|
102
117
|
if (!p || typeof p !== 'object' || Array.isArray(p)) return { ok: false, reason: 'not-object' }
|
|
103
118
|
if (typeof p.procedureId !== 'string' || !PROCEDURE_ID_RE.test(p.procedureId)) q.push('procedureId')
|
|
104
|
-
if (!
|
|
105
|
-
if (!
|
|
119
|
+
if (!PROCEDURE_STAGES_PRE_V1.includes(p.stage)) q.push('stage')
|
|
120
|
+
if (!PROCEDURE_RISKS_PRE_V1.includes(p.riskLevel)) q.push('riskLevel')
|
|
106
121
|
if (typeof p.title !== 'string' || !p.title.trim()) q.push('title')
|
|
107
122
|
if (!Array.isArray(p.sourceMemoryIds)) q.push('sourceMemoryIds')
|
|
108
123
|
if (!Array.isArray(p.sourceEpisodes)) q.push('sourceEpisodes')
|
|
@@ -118,6 +133,11 @@ export function validateProcedurePre(p) {
|
|
|
118
133
|
if (p.lastUsedAt !== undefined && (typeof p.lastUsedAt !== 'number' || !Number.isFinite(p.lastUsedAt))) q.push('lastUsedAt')
|
|
119
134
|
if (p.pinned !== undefined && typeof p.pinned !== 'boolean') q.push('pinned')
|
|
120
135
|
if (p.origin !== undefined && !['agent', 'user'].includes(p.origin)) q.push('origin')
|
|
136
|
+
// ★M8-B(2026-09-23):库归属与来源身份(均为可选,向后兼容旧快照)。
|
|
137
|
+
// 判据一律**白名单式**(不在枚举内即拒)—— 仓内既有纪律:不得用负向类型判断实现白名单门。
|
|
138
|
+
if (p.scope !== undefined && !PROCEDURE_SCOPES_PRE_V1.includes(p.scope)) q.push('scope')
|
|
139
|
+
if (p.workspaceRef !== undefined && typeof p.workspaceRef !== 'string') q.push('workspaceRef')
|
|
140
|
+
if (p.addedBy !== undefined && typeof p.addedBy !== 'string') q.push('addedBy')
|
|
121
141
|
// ★A-6(2026-09-21):evidence 形状校验。此前清单里**没有 evidence** ⇒
|
|
122
142
|
// ① 缺键时 promote() 的 `ev.seen + ev.read + …` 得 NaN,`NaN > cap` 为 false **静默放行**;
|
|
123
143
|
// ② evidence 整个缺失时 addEvidence 抛 TypeError,被宿主 lib/index.js:7644 空 catch 吞掉。
|
|
@@ -141,7 +161,7 @@ export function validateProcedureCandidatePre(c) {
|
|
|
141
161
|
const p = []
|
|
142
162
|
if (!c || typeof c !== 'object' || Array.isArray(c)) return { ok: false, reason: 'not-object' }
|
|
143
163
|
if (typeof c.title !== 'string' || !c.title.trim()) p.push('title')
|
|
144
|
-
if (!
|
|
164
|
+
if (!PROCEDURE_RISKS_PRE_V1.includes(c.riskLevel || 'low')) p.push('riskLevel')
|
|
145
165
|
if (!Array.isArray(c.steps) || !c.steps.length) p.push('steps')
|
|
146
166
|
if (c.successCriteria !== undefined && !Array.isArray(c.successCriteria)) p.push('successCriteria')
|
|
147
167
|
if (c.sourceMemoryIds !== undefined && !Array.isArray(c.sourceMemoryIds)) p.push('sourceMemoryIds')
|
|
@@ -150,6 +170,10 @@ export function validateProcedureCandidatePre(c) {
|
|
|
150
170
|
if (c.checks !== undefined && !Array.isArray(c.checks)) p.push('checks')
|
|
151
171
|
if (c.rollback !== undefined && !Array.isArray(c.rollback)) p.push('rollback')
|
|
152
172
|
if (c.observationOnly !== undefined && typeof c.observationOnly !== 'boolean') p.push('observationOnly')
|
|
173
|
+
// ★M8-B:候选也接受库归属与来源身份(校验口径与 validateProcedurePre 一致)。
|
|
174
|
+
if (c.scope !== undefined && !PROCEDURE_SCOPES_PRE_V1.includes(c.scope)) p.push('scope')
|
|
175
|
+
if (c.workspaceRef !== undefined && typeof c.workspaceRef !== 'string') p.push('workspaceRef')
|
|
176
|
+
if (c.addedBy !== undefined && typeof c.addedBy !== 'string') p.push('addedBy')
|
|
153
177
|
if (p.length) return { ok: false, reason: 'invalid:' + p.join(',') }
|
|
154
178
|
return { ok: true, candidate: c }
|
|
155
179
|
}
|
|
@@ -168,8 +192,8 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
168
192
|
const approveFn = typeof opts.approve === 'function' ? opts.approve : null
|
|
169
193
|
// 2026-08-30:gates 保留调用方对象引用(支持 getter 活读配置);缺省才用冻结默认。
|
|
170
194
|
// 注意:不可 Object.assign 展开——会立即求值 getter 并冻结挂载时的配置快照。
|
|
171
|
-
const gates = opts.gates ||
|
|
172
|
-
const activeLevel = opts.activeLevel ||
|
|
195
|
+
const gates = opts.gates || PROCEDURE_DEFAULT_GATES_PRE_V1
|
|
196
|
+
const activeLevel = opts.activeLevel || PROCEDURE_ACTIVE_LEVEL_PRE_V1
|
|
173
197
|
|
|
174
198
|
let procedures = [] // 全部 procedure(各 stage)
|
|
175
199
|
let disposed = false
|
|
@@ -197,7 +221,7 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
197
221
|
}
|
|
198
222
|
|
|
199
223
|
function defaultProcedureId(title, createdAt) {
|
|
200
|
-
const h = createHash('sha256').update(['procedure-v1', String(title), String(createdAt)].join('\u0000')).digest('hex')
|
|
224
|
+
const h = createHash('sha256').update(['procedure-pre-v1', String(title), String(createdAt)].join('\u0000')).digest('hex')
|
|
201
225
|
return PROCEDURE_ID_PREFIX + h.slice(0, 32)
|
|
202
226
|
}
|
|
203
227
|
|
|
@@ -241,6 +265,11 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
241
265
|
lastUsedAt: now,
|
|
242
266
|
pinned: false,
|
|
243
267
|
origin: c.origin === 'user' ? 'user' : 'agent',
|
|
268
|
+
// ★M8-B(2026-09-23):库归属 + 来源身份。缺省 `global` = 留在通用库,
|
|
269
|
+
// 与旧行为逐字节一致;要求进工作区库的调用方必须显式给 scope+workspaceRef。
|
|
270
|
+
scope: PROCEDURE_SCOPES_PRE_V1.includes(c.scope) ? c.scope : PROCEDURE_DEFAULT_SCOPE_PRE_V1,
|
|
271
|
+
workspaceRef: typeof c.workspaceRef === 'string' ? c.workspaceRef : '',
|
|
272
|
+
addedBy: typeof c.addedBy === 'string' ? c.addedBy : '',
|
|
244
273
|
// issue #30:纯 episode 观察行显式打标 —— 它没有 successCriteria,永远不该晋升,
|
|
245
274
|
// 也不该与富候选互相污染(此前无标记,只能靠 title 撞车后丢字段)。
|
|
246
275
|
...(isObservationOnlyPre(c) ? { observationOnly: true } : {}),
|
|
@@ -308,7 +337,7 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
308
337
|
* 只归档不删除;返回变更计数。 */
|
|
309
338
|
function applyAutomaticTransitions(nowTs) {
|
|
310
339
|
const now = Number.isFinite(nowTs) ? nowTs : nowFn()
|
|
311
|
-
const cutoff = now -
|
|
340
|
+
const cutoff = now - PROCEDURE_ARCHIVE_AFTER_DAYS_PRE_V1 * 86400000
|
|
312
341
|
const counts = { checked: 0, archived: 0 }
|
|
313
342
|
for (const p of procedures) {
|
|
314
343
|
counts.checked++
|
|
@@ -413,7 +442,7 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
413
442
|
* ⇒ 只读面必须走本函数(用户 2026-09-18 冻结纪律)。
|
|
414
443
|
*
|
|
415
444
|
* ⚠️ 维护约定:改 `promote()` 的门限时**必须同步改这里**,否则前端会展示过时的判据。
|
|
416
|
-
* 两者的一致性由测试 `smoke-test-r2-promotion
|
|
445
|
+
* 两者的一致性由测试 `smoke-test-r2-promotion.mjs` 用变异兜底。
|
|
417
446
|
*
|
|
418
447
|
* @returns {{ok:boolean, decision?:string, reasonCodes?:string[], detail?:object}}
|
|
419
448
|
*/
|
|
@@ -457,11 +486,40 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
457
486
|
return { ok: true, decision: 'promote', reasonCodes: reason, detail: { diversity, successCount } }
|
|
458
487
|
}
|
|
459
488
|
|
|
489
|
+
/**
|
|
490
|
+
* ★A-9b(2026-09-23):补判「被统计门 early-return 遮住」的结构门 —— 供只读越权投影使用。
|
|
491
|
+
*
|
|
492
|
+
* 与 `promote()` **逐条同源**(同字段、同 gates 常量、同判定式),只是**不提前 return**:
|
|
493
|
+
* 把统计门之后**所有**会拦人的结构门都取出来。取码顺序 = `promote()` 里的真实顺序
|
|
494
|
+
* (correction-rate → has-correction → no-success-criteria → 高风险),故
|
|
495
|
+
* `statCodes.concat(hidden)` 与「授权后走 promote()」吐出的原因码序列一致。
|
|
496
|
+
*
|
|
497
|
+
* ⚠️ 维护约定:`promote()` 统计门之后每加/改一道结构门,本函数必须同步(否则界面会
|
|
498
|
+
* 重新长出「点了没反应」的强制晋升按钮 —— 那正是 A-9b 要根治的事故)。
|
|
499
|
+
* 纯读,不写盘、不改状态、不动统计。
|
|
500
|
+
*
|
|
501
|
+
* @param {object} p 条目
|
|
502
|
+
* @param {object} g 门限(gates)
|
|
503
|
+
* @returns {string[]} 被遮结构门的原因码(无则空数组)
|
|
504
|
+
*/
|
|
505
|
+
function hiddenStructuralCodesPre(p, g) {
|
|
506
|
+
const out = []
|
|
507
|
+
const ev = (p && p.evidence) || {}
|
|
508
|
+
const num = (v) => Number(v) || 0
|
|
509
|
+
const total = num(ev.seen) + num(ev.read) + num(ev.cite) + num(ev.reuse) + num(ev.success) + num(ev.correction)
|
|
510
|
+
const corrRate = total > 0 ? num(ev.correction) / total : 0
|
|
511
|
+
if (corrRate > g.maxCorrectionRate) out.push('correction-rate-' + corrRate.toFixed(2))
|
|
512
|
+
if (g.maxContradictions === 0 && num(ev.correction) > 0) out.push('has-correction')
|
|
513
|
+
if (!p || !p.successCriteria || !p.successCriteria.length) out.push('no-success-criteria')
|
|
514
|
+
if (p && p.requiresApproval && !p.approved) out.push('high-risk-awaiting-approval')
|
|
515
|
+
return out
|
|
516
|
+
}
|
|
517
|
+
|
|
460
518
|
/**
|
|
461
519
|
* ★A/B(2026-09-22):**人工可越权性**只读投影 —— 回答「这道 keep 人能不能强制越过」。
|
|
462
520
|
*
|
|
463
521
|
* 为什么单独一个函数、而不是给 evaluatePromotion 加参数:
|
|
464
|
-
* · `smoke-test-r2-promotion
|
|
522
|
+
* · `smoke-test-r2-promotion.mjs` 用**字面正则**锁住了 `evaluatePromotion(procedureId)` 的签名,
|
|
465
523
|
* 并用**源码切片**比对它与 `promote()` 的 reason.push 序列;动它的签名或函数体会同时打红两条守卫。
|
|
466
524
|
* · 更要紧的是「不重复实现门限」:本函数**不重写任何一道门**,而是复用 evaluatePromotion 的结论,
|
|
467
525
|
* 只判断「拦它的原因码里是否**只有**那两道统计门」。
|
|
@@ -472,7 +530,23 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
472
530
|
* no-success-criteria / high-risk-awaiting-approval)**授权也拦**
|
|
473
531
|
* ⇒ 只要原因码里出现其中任何一个,就**不可越权**(界面上不该给强制按钮)。
|
|
474
532
|
*
|
|
475
|
-
*
|
|
533
|
+
* ★A-9b(2026-09-23 用户拍板「按推荐方案全修」):**必须补查被提前 return 遮住的结构门**。
|
|
534
|
+
*
|
|
535
|
+
* 事故(有代码证据):`evaluatePromotion` 的门限顺序是 diversity → success → correction-rate →
|
|
536
|
+
* has-correction → no-success-criteria → **高风险**,前一道不过就 early-return。于是
|
|
537
|
+
* 「证据不足 + 高风险未批准」的条目只吐出 `['diversity-below-3']` ⇒ 旧判据("码全是统计门")
|
|
538
|
+
* 得 `overridable:true` ⇒ 界面渲染「强制晋升」按钮;点下去 `promote()` 跳过统计门后**撞上高风险门**
|
|
539
|
+
* 返回 `ask`,用户所见 `force-promote: ask (user-authorized, high-risk-awaiting-approval)`
|
|
540
|
+
* 正是这条链 —— 一个点了没反应(没晋升)的假通道。
|
|
541
|
+
*
|
|
542
|
+
* 修法(**不动 evaluatePromotion 的签名/函数体/门限顺序** —— r2 用字面正则锁签名、
|
|
543
|
+
* 用源码切片锁顺序):在只读投影这一层,把「统计门之后仍会拦人的结构门」**独立补判一次**。
|
|
544
|
+
* 判据与 `promote()` 的对应门**逐条同源**(同一字段、同一 gates 常量),只是**取码顺序按
|
|
545
|
+
* promote() 真实顺序**,故本函数输出的 codes 序列与「授权后的 promote()」一致。
|
|
546
|
+
*
|
|
547
|
+
* ⚠️ 维护约定:改 `promote()` 统计门之后的任一结构门,必须同步改 `hiddenStructuralCodesPre`。
|
|
548
|
+
*
|
|
549
|
+
* 纯只读:只调 evaluatePromotion + 读条目字段,不写盘、不改状态、不动统计。异常一律 fail-soft。
|
|
476
550
|
*
|
|
477
551
|
* @param {string} procedureId
|
|
478
552
|
* @returns {{ok:boolean, decision?:string, overridable:boolean, reasonCodes?:string[], gateKind?:string}}
|
|
@@ -493,13 +567,23 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
493
567
|
const codes = Array.isArray(view.reasonCodes) ? view.reasonCodes : []
|
|
494
568
|
const isStat = (c) => /^diversity-below-/.test(c) || /^success-below-/.test(c)
|
|
495
569
|
const stat = codes.length > 0 && codes.every(isStat)
|
|
496
|
-
|
|
497
|
-
ok: true,
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
570
|
+
if (!stat) {
|
|
571
|
+
return { ok: true, decision: 'keep', overridable: false, gateKind: 'structural', reasonCodes: codes }
|
|
572
|
+
}
|
|
573
|
+
// ★A-9b:码全是统计门,但这些码是**提前 return 的结果**、不完整 —— 补查被遮结构门。
|
|
574
|
+
const p = procedures.find((x) => x.procedureId === procedureId)
|
|
575
|
+
if (!p) return { ok: false, overridable: false, gateKind: 'unknown' }
|
|
576
|
+
const hidden = hiddenStructuralCodesPre(p, gates)
|
|
577
|
+
if (hidden.length) {
|
|
578
|
+
return {
|
|
579
|
+
ok: true,
|
|
580
|
+
decision: 'keep',
|
|
581
|
+
overridable: false,
|
|
582
|
+
gateKind: 'structural',
|
|
583
|
+
reasonCodes: codes.concat(hidden),
|
|
584
|
+
}
|
|
502
585
|
}
|
|
586
|
+
return { ok: true, decision: 'keep', overridable: true, gateKind: 'statistical', reasonCodes: codes }
|
|
503
587
|
} catch (_) {
|
|
504
588
|
return { ok: false, overridable: false, gateKind: 'unknown' }
|
|
505
589
|
}
|
|
@@ -644,7 +728,7 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
644
728
|
// ---- 持久化 ----
|
|
645
729
|
function snapshot() {
|
|
646
730
|
return {
|
|
647
|
-
schemaVersion: 1, namespace: 'dsh-auto-memory', policyVersion: PROCEDURE_POLICY_VERSION,
|
|
731
|
+
schemaVersion: 1, namespace: 'dsh-auto-memory-pre', policyVersion: PROCEDURE_POLICY_VERSION,
|
|
648
732
|
savedAt: nowFn(),
|
|
649
733
|
procedures: procedures.map((p) => ({ ...p, _sessions: p._sessions ? [...p._sessions] : undefined, evidence: { ...p.evidence } })),
|
|
650
734
|
}
|
|
@@ -666,6 +750,11 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
666
750
|
}
|
|
667
751
|
// issue #30:旧快照里没有 observationOnly 字段,恢复时按同一判据补齐
|
|
668
752
|
// (纯增量元数据迁移:不改身份/文本/风险/证据/阶段)。
|
|
753
|
+
// ★M8-B:旧快照没有库归属字段 —— 恢复时按缺省语义补齐为「通用库」,
|
|
754
|
+
// 使下游 IO 分派与前端分组都有确定值(纯增量元数据迁移,不改身份/文本/证据/阶段)。
|
|
755
|
+
if (v.procedure.scope === undefined) v.procedure.scope = PROCEDURE_DEFAULT_SCOPE_PRE_V1
|
|
756
|
+
if (v.procedure.workspaceRef === undefined) v.procedure.workspaceRef = ''
|
|
757
|
+
if (v.procedure.addedBy === undefined) v.procedure.addedBy = ''
|
|
669
758
|
procedures.push(normalizeProcedureObservationPre(v.procedure))
|
|
670
759
|
}
|
|
671
760
|
// ★B-1(2026-09-21):stats.approved 是本轮新增计数,旧快照(以及本模块自己的快照 ——
|
package/lib/python-setup.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { memoryDir } from './datadir.js'
|
|
1
2
|
/**
|
|
2
3
|
* M7.6 Python 一键向导(host 半):#16-#20 配套——把 C3 进阶档从"开发机可达"变成"爱好者可达"。
|
|
3
4
|
*
|
|
@@ -53,8 +54,8 @@ const PIP_DEPS_GPU_EXTRA = ['onnxruntime-gpu']
|
|
|
53
54
|
* 历史上探针多要一个 torch,而 PIP_DEPS_CPU 从不装 torch → depsOk 恒 false,"全部就绪"永不出现。 */
|
|
54
55
|
const DEPS_PROBE = 'import transformers, onnxruntime, numpy; print("deps-ok")'
|
|
55
56
|
|
|
56
|
-
/** worker 侧 load_embedder 的档位名(
|
|
57
|
-
const PROVIDER_ID_INT8 = 'bge-m3-onnx-int8-v1'
|
|
57
|
+
/** worker 侧 load_embedder 的档位名(m7_embedding_pre_v1.PROVIDER_REAL_INT8);发布构建会把 -pre-v1 折成 -v1。 */
|
|
58
|
+
const PROVIDER_ID_INT8 = 'bge-m3-onnx-int8-pre-v1'
|
|
58
59
|
/** dense 维度(BGE-M3 = 1024),写进 embedding-config.json 的 dimension。 */
|
|
59
60
|
const EMBED_DIMENSION = 1024
|
|
60
61
|
|
|
@@ -101,10 +102,10 @@ export function createPythonSetupPre(opts = {}) {
|
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
// ---------- 引擎侧 embedding-config.json(D1/D2:向导产物必须落在 worker 真正读取的位置与键上) ----------
|
|
104
|
-
/** worker 读 <dsh-home>/memory/semantic/embedding-config.json(见
|
|
105
|
-
* 发布构建会把 `semantic` 折成 `semantic`,故这里写带 -pre 的规范名。
|
|
105
|
+
/** worker 读 <dsh-home>/memory/semantic-pre/embedding-config.json(见 worker_semantic_pre_v1.load_embedding_config_from_env);
|
|
106
|
+
* 发布构建会把 `semantic-pre` 折成 `semantic`,故这里写带 -pre 的规范名。
|
|
106
107
|
* 历史缺陷:向导曾写 <dsh-home>/memory/semantic/(缺 -pre),pre 线 worker 从不读该路径 → 装完即"找不到配置"。 */
|
|
107
|
-
const embeddingConfigPath = () => path.join(
|
|
108
|
+
const embeddingConfigPath = () => path.join(memoryDir('semantic', dshHomeOf), 'embedding-config.json')
|
|
108
109
|
|
|
109
110
|
/** read-modify-write:只覆盖本次传入的键,保留引擎运行期自管的 search/activationPolicy/activationEmitMode 等。 */
|
|
110
111
|
async function patchEmbeddingConfig(patch) {
|
|
@@ -28,18 +28,18 @@ import { randomBytes } from 'node:crypto'
|
|
|
28
28
|
import path from 'node:path'
|
|
29
29
|
import { fileURLToPath } from 'node:url'
|
|
30
30
|
import {
|
|
31
|
-
validateTransportFramePre, makeRequestFramePre,
|
|
32
|
-
|
|
31
|
+
validateTransportFramePre, makeRequestFramePre, RESPONSE_TYPE_FOR_PRE_V1,
|
|
32
|
+
M7_TRANSPORT_BUDGET_PRE_V1, PY_FRAME_TYPES_PRE_V1,
|
|
33
33
|
} from './m7-wire.js'
|
|
34
34
|
|
|
35
|
-
const B =
|
|
35
|
+
const B = M7_TRANSPORT_BUDGET_PRE_V1
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
|
-
* 看门狗/诊断策略(**客户端本地策略量**,刻意不放进
|
|
38
|
+
* 看门狗/诊断策略(**客户端本地策略量**,刻意不放进 M7_TRANSPORT_BUDGET_PRE_V1)。
|
|
39
39
|
* 理由有二:①传输预算按契约冻结、变更须升协议版本,而「多久判挂死」是客户端自愈策略、不是 wire 契约;
|
|
40
40
|
* ②wire 文件同期另有 PR(#122/#124)在改,本任务不去碰它以避开写域冲突。
|
|
41
41
|
*/
|
|
42
|
-
const
|
|
42
|
+
const SIDECAR_WATCHDOG_PRE_V1 = Object.freeze({
|
|
43
43
|
stallThreshold: 3, // 连续超时(心跳缺失)达此数 ⇒ 判定挂死,看门狗击杀旧进程
|
|
44
44
|
stderrTailChars: 4096, // stderr 环形尾部保留长度(既有语义,原为字面量 4096)
|
|
45
45
|
diagRingCapacity: 16, // diag 环形缓冲条数(超出丢最旧)
|
|
@@ -48,7 +48,7 @@ const SIDECAR_WATCHDOG_V1 = Object.freeze({
|
|
|
48
48
|
})
|
|
49
49
|
|
|
50
50
|
/** 退出归因优先级:高优先级不被低优先级覆盖。 */
|
|
51
|
-
const
|
|
51
|
+
const EXIT_KIND_RANK_PRE_V1 = Object.freeze({ crash: 0, restart: 1, dispose: 1, fatal: 2, watchdog: 3 })
|
|
52
52
|
|
|
53
53
|
/**
|
|
54
54
|
* ★ #107 熔断清零口径:只有「终态成功」才把累计失败清零。
|
|
@@ -57,12 +57,12 @@ const EXIT_KIND_RANK_V1 = Object.freeze({ crash: 0, restart: 1, dispose: 1, fata
|
|
|
57
57
|
* 成功就把累计失败抹平,峰值恒 1、阈值 3(breakerFailureThreshold)永不达,熔断形同虚设。
|
|
58
58
|
* health / context_push / index_sync_commit 才是各自工作单元的终态应答。
|
|
59
59
|
*/
|
|
60
|
-
const
|
|
60
|
+
const TERMINAL_SUCCESS_TYPES_PRE_V1 = new Set(['health', 'context_push', 'index_sync_commit'])
|
|
61
61
|
|
|
62
|
-
/** 捆绑 fake worker 的默认绝对路径(python/
|
|
62
|
+
/** 捆绑 fake worker 的默认绝对路径(python/worker_pre_v1.py)。 */
|
|
63
63
|
export function defaultWorkerScriptPathPre() {
|
|
64
64
|
try {
|
|
65
|
-
return path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'python', '
|
|
65
|
+
return path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'python', 'worker_pre_v1.py')
|
|
66
66
|
} catch (_) { return '' }
|
|
67
67
|
}
|
|
68
68
|
|
|
@@ -141,9 +141,9 @@ export function createPythonSidecarClientPre(opts = {}) {
|
|
|
141
141
|
|
|
142
142
|
/** 有界 diag 环形缓冲:落一条**给人看**的中文诊断;opts.onDiag 在场时同步外发一份。 */
|
|
143
143
|
function pushDiag(level, text) {
|
|
144
|
-
const rec = { at: Date.now(), level, sidecar: 'python-sidecar
|
|
144
|
+
const rec = { at: Date.now(), level, sidecar: 'python-sidecar', generation, message: String(text) }
|
|
145
145
|
diagRing.push(rec)
|
|
146
|
-
while (diagRing.length > Math.max(1, Number(opt('diagRingCapacity',
|
|
146
|
+
while (diagRing.length > Math.max(1, Number(opt('diagRingCapacity', SIDECAR_WATCHDOG_PRE_V1.diagRingCapacity)))) diagRing.shift()
|
|
147
147
|
const sink = resolveOpt('onDiag')
|
|
148
148
|
if (typeof sink === 'function') { try { sink({ at: rec.at, level: rec.level, generation, message: rec.message }) } catch (_) {} }
|
|
149
149
|
return rec
|
|
@@ -161,8 +161,8 @@ export function createPythonSidecarClientPre(opts = {}) {
|
|
|
161
161
|
// 刻意不用正则字面量:本仓所有 .js 均为 CRLF,避免任何转义歧义。
|
|
162
162
|
const all = src.split(NL).map((l) => (l.endsWith(CR) ? l.slice(0, -1) : l))
|
|
163
163
|
while (all.length && all[all.length - 1] === '') all.pop()
|
|
164
|
-
const maxLines = Math.max(1, Number(opt('diagTailLines',
|
|
165
|
-
const maxChars = Math.max(1, Number(opt('diagTailChars',
|
|
164
|
+
const maxLines = Math.max(1, Number(opt('diagTailLines', SIDECAR_WATCHDOG_PRE_V1.diagTailLines)))
|
|
165
|
+
const maxChars = Math.max(1, Number(opt('diagTailChars', SIDECAR_WATCHDOG_PRE_V1.diagTailChars)))
|
|
166
166
|
const kept = all.slice(-maxLines)
|
|
167
167
|
let truncated = kept.length < all.length
|
|
168
168
|
let text = kept.join(NL)
|
|
@@ -187,7 +187,7 @@ export function createPythonSidecarClientPre(opts = {}) {
|
|
|
187
187
|
function markExitKind(gen, kind) {
|
|
188
188
|
if (!gen) return
|
|
189
189
|
const cur = exitKindByGen.get(gen) || 'crash'
|
|
190
|
-
if ((
|
|
190
|
+
if ((EXIT_KIND_RANK_PRE_V1[kind] || 0) < (EXIT_KIND_RANK_PRE_V1[cur] || 0)) return
|
|
191
191
|
exitKindByGen.set(gen, kind)
|
|
192
192
|
while (exitKindByGen.size > 64) exitKindByGen.delete(exitKindByGen.keys().next().value)
|
|
193
193
|
}
|
|
@@ -247,7 +247,7 @@ export function createPythonSidecarClientPre(opts = {}) {
|
|
|
247
247
|
stats.watchdog.lastReason = trigger
|
|
248
248
|
stats.watchdog.lastAt = Date.now()
|
|
249
249
|
stats.watchdog.lastDetail = detail || null
|
|
250
|
-
const thr = Math.max(1, Number(opt('watchdogStallThreshold',
|
|
250
|
+
const thr = Math.max(1, Number(opt('watchdogStallThreshold', SIDECAR_WATCHDOG_PRE_V1.stallThreshold)))
|
|
251
251
|
pushDiag('error', '第 ' + generation + ' 代 Python 语义侧车连续 ' + thr + ' 次心跳缺失(' + trigger + ')⇒ 看门狗主动击杀旧进程;下次请求将重生新进程(新 epoch),未经确认的旧 in-flight 请求按 crashed 结算。')
|
|
252
252
|
logExitDiag(generation, 'watchdog', humanExitNote(generation, 'watchdog', null, null))
|
|
253
253
|
try { proc.stdin.destroy() } catch (_) {}
|
|
@@ -258,7 +258,7 @@ export function createPythonSidecarClientPre(opts = {}) {
|
|
|
258
258
|
/** 心跳缺失证据:只把「超时」算挂死(abort 是调用方主动取消,不构成挂死证据)。 */
|
|
259
259
|
function noteStall(type, requestId, timeoutMs) {
|
|
260
260
|
consecutiveTimeouts++
|
|
261
|
-
const thr = Math.max(1, Number(opt('watchdogStallThreshold',
|
|
261
|
+
const thr = Math.max(1, Number(opt('watchdogStallThreshold', SIDECAR_WATCHDOG_PRE_V1.stallThreshold)))
|
|
262
262
|
if (consecutiveTimeouts < thr) return
|
|
263
263
|
disarmHungChild('连续超时', { type: String(type), requestId: String(requestId), timeoutMs: Number(timeoutMs), consecutiveTimeouts })
|
|
264
264
|
}
|
|
@@ -305,7 +305,7 @@ export function createPythonSidecarClientPre(opts = {}) {
|
|
|
305
305
|
const scriptPath = String(resolveOpt('scriptPath') || defaultWorkerScriptPathPre())
|
|
306
306
|
const command = String(resolveOpt('command') || 'python')
|
|
307
307
|
const dshHome = String(resolveOpt('dshHome') || '')
|
|
308
|
-
epoch = '
|
|
308
|
+
epoch = 'wk_pre_' + randomBytes(16).toString('hex')
|
|
309
309
|
const args = [scriptPath, '--expect-epoch', epoch]
|
|
310
310
|
if (dshHome) args.push('--dsh-home', dshHome)
|
|
311
311
|
let proc
|
|
@@ -327,7 +327,7 @@ export function createPythonSidecarClientPre(opts = {}) {
|
|
|
327
327
|
proc.stdout.on('data', (chunk) => { try { feed(chunk) } catch (_) { fatal('protocol') } })
|
|
328
328
|
proc.stderr.on('data', (chunk) => {
|
|
329
329
|
stderrTail = (stderrTail + chunk.toString('utf8'))
|
|
330
|
-
.slice(-Math.max(256, Number(opt('stderrTailChars',
|
|
330
|
+
.slice(-Math.max(256, Number(opt('stderrTailChars', SIDECAR_WATCHDOG_PRE_V1.stderrTailChars))))
|
|
331
331
|
})
|
|
332
332
|
proc.on('error', () => {
|
|
333
333
|
// spawn 失败(ENOENT 等):结构化失败,不计崩溃重启
|
|
@@ -406,7 +406,7 @@ export function createPythonSidecarClientPre(opts = {}) {
|
|
|
406
406
|
// (未启动 / `exit` 事件已置 null 但旧 stdio 尚未排空 / restart 后到新 spawn 之间),
|
|
407
407
|
// 而 `activation_request` 上游 `context-host` **无二次校验**(同 activationId 有 dedup 兜底,
|
|
408
408
|
// 但**新 activationId 可穿透**)⇒ 幽灵激活。
|
|
409
|
-
// 新判据:**epoch 为空 ⇒ 一律丢弃**。理由:spawn 时同步赋 epoch(:91 `epoch = '
|
|
409
|
+
// 新判据:**epoch 为空 ⇒ 一律丢弃**。理由:spawn 时同步赋 epoch(:91 `epoch = 'wk_pre_' + …` 在
|
|
410
410
|
// `spawn()` **之前**执行),出站帧也就带上了 epoch;对端 worker 以 `--expect-epoch` 自证身份后才回帧
|
|
411
411
|
// ⇒ **正常路径不存在"帧先于 epoch"**,丢弃不会误伤任何合法流量。
|
|
412
412
|
if (!epoch || frame.workerEpoch !== epoch) { stats.dropped.staleEpoch++; return }
|
|
@@ -447,7 +447,7 @@ export function createPythonSidecarClientPre(opts = {}) {
|
|
|
447
447
|
// ★ #107 熔断清零口径:只有「终态成功」清零累计失败;段内进度帧(index_sync_begin /
|
|
448
448
|
// index_sync_page)的 ack 成功**不清零** —— 否则每个 Segment 一次 begin 就把累计失败
|
|
449
449
|
// 抹平,峰值恒 1、阈值 3 永不达,熔断形同虚设。
|
|
450
|
-
if (
|
|
450
|
+
if (TERMINAL_SUCCESS_TYPES_PRE_V1.has(entry.type)) {
|
|
451
451
|
breaker.consecutiveFailures = 0
|
|
452
452
|
consecutiveTimeouts = 0
|
|
453
453
|
halfOpenPending = false
|
|
@@ -489,7 +489,7 @@ export function createPythonSidecarClientPre(opts = {}) {
|
|
|
489
489
|
*/
|
|
490
490
|
function request(type, payload, rOpts = {}) {
|
|
491
491
|
if (disposed) return Promise.resolve({ ok: false, code: 'disposed' })
|
|
492
|
-
if (!
|
|
492
|
+
if (!RESPONSE_TYPE_FOR_PRE_V1[type]) return Promise.resolve({ ok: false, code: 'unsupported-frame' })
|
|
493
493
|
if (breakerOpen()) {
|
|
494
494
|
stats.failed.circuitOpen++
|
|
495
495
|
return Promise.resolve({ ok: false, code: 'circuit-open', retryInMs: breaker.openUntil - Date.now() })
|
|
@@ -514,12 +514,12 @@ export function createPythonSidecarClientPre(opts = {}) {
|
|
|
514
514
|
}
|
|
515
515
|
const maxPending = Number(opt('maxPendingRequests', B.maxPendingRequests))
|
|
516
516
|
if (pending.size >= maxPending) { stats.failed.backpressure++; return Promise.resolve({ ok: false, code: 'backpressure' }) }
|
|
517
|
-
const requestId = '
|
|
517
|
+
const requestId = 'req_pre_' + randomBytes(9).toString('hex') + (++reqCounter).toString(36)
|
|
518
518
|
const sentAt = Date.now()
|
|
519
519
|
const mf = makeRequestFramePre({ type, payload, requestId, workerEpoch: epoch, sentAt })
|
|
520
520
|
if (!mf.ok) { stats.failed.protocol++; return Promise.resolve({ ok: false, code: 'protocol', reason: mf.reason }) }
|
|
521
521
|
return new Promise((resolve) => {
|
|
522
|
-
const entry = { requestId, type, expectedType:
|
|
522
|
+
const entry = { requestId, type, expectedType: RESPONSE_TYPE_FOR_PRE_V1[type], resolve, settled: false, timer: null, signal: rOpts.signal || null, onAbort: null }
|
|
523
523
|
pending.set(requestId, entry)
|
|
524
524
|
stats.requests++
|
|
525
525
|
const written = writeFrame(mf.frame)
|
|
@@ -549,9 +549,9 @@ export function createPythonSidecarClientPre(opts = {}) {
|
|
|
549
549
|
|
|
550
550
|
/** fire-and-forget 帧(cancel/close_session;契约上无响应帧)。 */
|
|
551
551
|
function notify(type, payload) {
|
|
552
|
-
if (disposed || !
|
|
552
|
+
if (disposed || !PY_FRAME_TYPES_PRE_V1) return
|
|
553
553
|
if (!child) return
|
|
554
|
-
const requestId = '
|
|
554
|
+
const requestId = 'ntf_pre_' + randomBytes(6).toString('hex')
|
|
555
555
|
const sentAt = Date.now()
|
|
556
556
|
const mf = makeRequestFramePre({ type, payload, requestId, workerEpoch: epoch, sentAt })
|
|
557
557
|
if (mf.ok) { if (writeFrame(mf.frame)) stats.cancelNotifications++ }
|
|
@@ -615,7 +615,7 @@ export function createPythonSidecarClientPre(opts = {}) {
|
|
|
615
615
|
}
|
|
616
616
|
|
|
617
617
|
return {
|
|
618
|
-
kind: 'python-sidecar
|
|
618
|
+
kind: 'python-sidecar',
|
|
619
619
|
request, notify, health, restart, dispose, debugView,
|
|
620
620
|
ensureStarted, isStarted, currentEpoch, processForTest, breakerOpenForTest: breakerOpen,
|
|
621
621
|
// 测试钩子:确定性 framing 注入(partial/multiple/bad JSON/oversize/伪造帧)与最后出站帧检查
|
package/lib/recall-fusion.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* recall-fusion-pre —— rank-space 融合(P3, 2026-09-09;
|
|
2
|
+
* recall-fusion-pre —— rank-space 融合(P3, 2026-09-09; rrf_fusion_pre_v1)。
|
|
3
3
|
*
|
|
4
4
|
* 背景:旧 fuseD6Pre(semantic-js.js)为 minmax 加权融合,存在三宗罪:
|
|
5
5
|
* ① 分数随候选集漂移(归一化域=当前候选集) ② 矮子里拔将军(零极差臂全员 0.5)
|
|
@@ -16,15 +16,15 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
/** RRF 常数 k(Hindsight issue #3956 实测: k=60 时动态范围安全,加权会退化排序)。 */
|
|
19
|
-
export const
|
|
19
|
+
export const FUSION_RRF_K_PRE_V1 = 60
|
|
20
20
|
|
|
21
21
|
/** rank 尺度常数:rank/divisor 把秩归到 (0,1] 量级(默认与 k 同值)。 */
|
|
22
|
-
export const
|
|
22
|
+
export const FUSION_RRF_DIVISOR_PRE_V1 = 60
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
25
|
* ★M2.5b:层次优先级序(与注入侧一致)。
|
|
26
26
|
*
|
|
27
|
-
* 出处:`tier-layer-inject.js:58
|
|
27
|
+
* 出处:`tier-layer-inject.js:58 TIER_LAYER_ORDER_PRE_V1` —— **必须与之逐字相同**。
|
|
28
28
|
*
|
|
29
29
|
* 为什么不直接 import:本模块头注释承诺「**零依赖**、纯函数、零 IO」,
|
|
30
30
|
* 引入跨模块依赖会破坏该契约(且 tier-layer-inject 自身还有依赖链)。
|
|
@@ -32,10 +32,10 @@ export const FUSION_RRF_DIVISOR_V1 = 60
|
|
|
32
32
|
* (见 smoke-test-m25b-layer-arm-pre.mjs 的「常量一致性」断言)——
|
|
33
33
|
* 任何一侧改动而另一侧未同步,测试立刻红。
|
|
34
34
|
*/
|
|
35
|
-
export const
|
|
35
|
+
export const FUSION_LAYER_ORDER_PRE_V1 = Object.freeze(['project', 'whiteboard', 'user', 'reflection', 'log'])
|
|
36
36
|
|
|
37
37
|
/** 版本标识。 */
|
|
38
|
-
export const RECALL_FUSION_VERSION = '
|
|
38
|
+
export const RECALL_FUSION_VERSION = 'rrf_fusion_pre_v1'
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
41
|
* rank-space RRF 融合。
|
|
@@ -53,8 +53,8 @@ export const RECALL_FUSION_VERSION = 'rrf_fusion_v1'
|
|
|
53
53
|
* - 缺失臂贡献 0(不是 minmax 的 0.5)
|
|
54
54
|
*/
|
|
55
55
|
export function rankFusionRRFPre(pairs, opts = {}) {
|
|
56
|
-
const k = Number.isFinite(opts.k) && opts.k >= 0 ? opts.k :
|
|
57
|
-
const divisor = Number.isFinite(opts.divisor) && opts.divisor > 0 ? opts.divisor :
|
|
56
|
+
const k = Number.isFinite(opts.k) && opts.k >= 0 ? opts.k : FUSION_RRF_K_PRE_V1
|
|
57
|
+
const divisor = Number.isFinite(opts.divisor) && opts.divisor > 0 ? opts.divisor : FUSION_RRF_DIVISOR_PRE_V1
|
|
58
58
|
const list = Array.isArray(pairs) ? pairs.filter((p) => p && typeof p.memoryId === 'string') : []
|
|
59
59
|
|
|
60
60
|
// 每臂独立排名:非空有限值按分数降序(平局 memoryId 升序)取秩(1 起);缺席者无秩。
|
|
@@ -79,15 +79,15 @@ export function rankFusionRRFPre(pairs, opts = {}) {
|
|
|
79
79
|
|
|
80
80
|
/**
|
|
81
81
|
* ★M2.5b:层次臂的**序** = 注入侧的层次优先级序(二者必须一致)。
|
|
82
|
-
* 出处:`tier-layer-inject.js:58
|
|
82
|
+
* 出处:`tier-layer-inject.js:58 TIER_LAYER_ORDER_PRE_V1`
|
|
83
83
|
* = ['project','whiteboard','user','reflection','log']。
|
|
84
84
|
* 语义:**rank 1 = 最高优先级层**。未列出的层(含新增层)排在已知层之后,按名称升序(确定性)。
|
|
85
85
|
* 注意:这里只定义**偏好顺序**,不定义权重;权重问题走 rank-space(见下),
|
|
86
86
|
* 需要调权重时应改分档/序,而不是给分数乘系数(Hindsight #3956)。
|
|
87
87
|
*/
|
|
88
88
|
const layerRankOf = (name) => {
|
|
89
|
-
const i =
|
|
90
|
-
return i >= 0 ? i :
|
|
89
|
+
const i = FUSION_LAYER_ORDER_PRE_V1.indexOf(name)
|
|
90
|
+
return i >= 0 ? i : FUSION_LAYER_ORDER_PRE_V1.length
|
|
91
91
|
}
|
|
92
92
|
const cmpLayer = (a, b) => {
|
|
93
93
|
const la = layerRankOf(String(a.v))
|
|
@@ -106,7 +106,7 @@ export function rankFusionRRFPre(pairs, opts = {}) {
|
|
|
106
106
|
// 所有 pair 均缺 layer **或 layer 全相等** → 不构建 layer 秩,行为与三臂版**逐字节一致**
|
|
107
107
|
// (fused 不加 0 项、输出对象不含新字段)。
|
|
108
108
|
// 为什么需要它(实测不一致,2026-09-18):Tier-0 **注入**侧有层次优先级
|
|
109
|
-
// (tier-layer-inject.js:58
|
|
109
|
+
// (tier-layer-inject.js:58 TIER_LAYER_ORDER_PRE_V1 = ['project','whiteboard','user','reflection','log']
|
|
110
110
|
// + tier0-catalog.js:419-455 的 caps/floors),而 **排序**侧对层次完全无感
|
|
111
111
|
// ⇒ 结论层(project)与流水层(log)在融合里**平等竞争**,笔记层常被日志挤掉。
|
|
112
112
|
// rank-space(Hindsight #3956 禁止 score-space 加权):层次只贡献**秩**,不乘分。
|
package/lib/recall-stats.js
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, rmSync } from 'node:fs'
|
|
23
23
|
import path from 'node:path'
|
|
24
24
|
|
|
25
|
-
export const RECALL_STATS_VERSION_PRE = '
|
|
25
|
+
export const RECALL_STATS_VERSION_PRE = 'recall_stats_pre_v1'
|
|
26
26
|
|
|
27
27
|
/** 三条通路的稳定 id(写盘/前端都按它索引,禁止改拼写)。 */
|
|
28
28
|
export const RECALL_CHANNELS_PRE = ['model', 'inject', 'shadow']
|
|
@@ -32,6 +32,17 @@ const MAX_ENTRIES = 2000
|
|
|
32
32
|
/** 保留天数(按天分布只留最近 N 天)。 */
|
|
33
33
|
const MAX_DAYS = 90
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* ★A-11b(2026-09-23 用户拍板「按推荐方案全修」):落盘**节流**间隔(毫秒)。
|
|
37
|
+
*
|
|
38
|
+
* 事故(有硬证据):`observe()` / `observeInjection()` 此前只置 `dirty = true`,
|
|
39
|
+
* **全仓没有任何地方调 `.save()`**(只有 `reset()` 会)⇒ `~/.dsh/memory/recall-stats.json`
|
|
40
|
+
* 文件根本不存在 ⇒ 宿主一重启统计全清零(面板上看到的一直是"当前进程内"的数)。
|
|
41
|
+
* 修法:observe 尾部走 `maybeSave()` 节流落盘 —— 复用既有 `save()`,不新增开关。
|
|
42
|
+
* 间隔 2s:注入通路每轮调一次,节流后写盘成本可忽略,且重启最多丢最后 2s 的增量。
|
|
43
|
+
*/
|
|
44
|
+
const SAVE_THROTTLE_MS = 2000
|
|
45
|
+
|
|
35
46
|
/**
|
|
36
47
|
* @param {{file: () => string, now?: () => number}} opts
|
|
37
48
|
* `file()` 每次调用都重新解析路径(DSH_HOME 可能变),必须是函数而非字符串。
|
|
@@ -41,6 +52,8 @@ export function createRecallStatsPre(opts = {}) {
|
|
|
41
52
|
const fileOf = typeof opts.file === 'function' ? opts.file : () => ''
|
|
42
53
|
let state = null
|
|
43
54
|
let dirty = false
|
|
55
|
+
// ★A-11b(2026-09-23):上次落盘时刻,用于 observe 侧节流(见 maybeSave)。
|
|
56
|
+
let lastSaveAt = 0
|
|
44
57
|
|
|
45
58
|
const dayOf = (ms) => {
|
|
46
59
|
const d = new Date(ms)
|
|
@@ -118,6 +131,27 @@ export function createRecallStatsPre(opts = {}) {
|
|
|
118
131
|
}
|
|
119
132
|
}
|
|
120
133
|
|
|
134
|
+
/**
|
|
135
|
+
* ★A-11b(2026-09-23):**节流落盘** —— observe 尾部唯一的写盘通路。
|
|
136
|
+
*
|
|
137
|
+
* 为什么必须有:本模块此前只 `dirty = true`,`save()` 除 `reset()` 外**无任何调用方**
|
|
138
|
+
* ⇒ 统计从不落盘、重启即清零(面板上"召回统计不更新"的真因之一)。
|
|
139
|
+
* 为什么节流:inject 通路**每轮**都会调 `observeInjection`,不节流就是每轮一次原子写。
|
|
140
|
+
* 节流窗口内只置 dirty,下次 observe 或显式 `save()`(含 reset)时补写 —— 不丢数据,
|
|
141
|
+
* 只把「多次增量」合并成「一次写」。`nowFn` 可注入 ⇒ 测试里能确定性地推进时钟。
|
|
142
|
+
*
|
|
143
|
+
* fail-soft:写失败不抛出(save() 内部已 try/catch,返回 false)。
|
|
144
|
+
* @returns {boolean} 本次是否真的写了盘
|
|
145
|
+
*/
|
|
146
|
+
function maybeSave() {
|
|
147
|
+
if (!dirty) return false
|
|
148
|
+
const t = nowFn()
|
|
149
|
+
if (lastSaveAt && t - lastSaveAt < SAVE_THROTTLE_MS) return false
|
|
150
|
+
const ok = save()
|
|
151
|
+
if (ok) lastSaveAt = t
|
|
152
|
+
return ok
|
|
153
|
+
}
|
|
154
|
+
|
|
121
155
|
/** 裁剪:**每通道**条目数上限 + 天数上限。按 count 升序淘汰最冷门的。 */
|
|
122
156
|
function prune() {
|
|
123
157
|
for (const ch of RECALL_CHANNELS_PRE) {
|
|
@@ -177,6 +211,7 @@ export function createRecallStatsPre(opts = {}) {
|
|
|
177
211
|
}
|
|
178
212
|
prune()
|
|
179
213
|
dirty = true
|
|
214
|
+
maybeSave()
|
|
180
215
|
return true
|
|
181
216
|
} catch (_) { return false } // fail-soft:统计绝不影响召回
|
|
182
217
|
}
|