@a9i5k4/dsh-auto-memory 2.5.3 → 3.0.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 +171 -1
- package/README.zh-CN.md +171 -1
- package/docs/CONTRIBUTORS.html +471 -0
- package/docs/HANDOFF-CRITERIA.md +92 -0
- package/docs/INTEGRATION-ANALYSIS.md +350 -348
- package/docs/USER-GUIDE.en.md +56 -1
- package/docs/USER-GUIDE.zh-CN.md +57 -2
- package/docs/internal/ACCEPT-35-LIVE.md +143 -0
- package/docs/internal/ACCEPTANCE-20260914.md +90 -0
- package/docs/internal/ARCH-REVIEW-BRIEF.md +411 -0
- package/docs/internal/ARCH-REVIEW-REQUEST.md +201 -0
- package/docs/internal/ARCH-REVIEW-ROUND2.md +169 -0
- package/docs/internal/ARCH-REVIEW-ROUND3.md +206 -0
- package/docs/internal/AUDIT-WB-GRAPH-FULL-20260916.md +314 -0
- package/docs/internal/CONCURRENCY-INVESTIGATION-20260917.md +192 -0
- package/docs/internal/CROSS-SESSION-SEARCH-PATH-DECISION.md +72 -0
- package/docs/internal/CROSS-SESSION-SEARCH-RESEARCH.md +131 -0
- package/docs/internal/DECISIONS-20260914-SESSION.md +269 -0
- package/docs/internal/DESIGN-P1-STATE-COMMIT-20260915.md +219 -0
- package/docs/internal/DIRECTION-CHECK-WB-GRAPH-20260916.md +132 -0
- package/docs/internal/FEEDBACK-TO-DSHAPI-RELAY.md +13 -0
- package/docs/internal/GH-DISCUSSION-5732-COMMENT.md +74 -0
- package/docs/internal/GPT-ACCEPTANCE-PROMPT-20260916.md +352 -0
- package/docs/internal/GPT-REVIEW-PROMPT.md +216 -0
- package/docs/internal/GROUP-WEBHOOK-SETUP.md +33 -0
- package/docs/internal/KICKOFF-P0.md +254 -0
- package/docs/internal/MASTER-PLAN-3.0.md +411 -0
- package/docs/internal/MEMORY-MUTATION-AND-INDEX-DESIGN.md +85 -0
- package/docs/internal/MERGE-CONFLICT-SCAN-20260914.md +222 -0
- package/docs/internal/PENDING-FIXES-20260916.md +289 -0
- package/docs/internal/RAG-KARPATHY-PROGRAM.md +229 -0
- package/docs/internal/REPORT-P0-NIGHTLY.md +212 -0
- package/docs/internal/REPORT-P5-ACCEPTANCE.md +31 -0
- package/docs/internal/REPORT-WB-GRAPH-NIGHTLY.md +153 -0
- package/docs/internal/REVIEW-WB-GRAPH-SELF.md +81 -0
- package/docs/internal/ROADMAP-20260917-WEEK.md +305 -0
- package/docs/internal/ROADMAP.md +106 -0
- package/docs/internal/RUN-P0-NIGHTLY.md +227 -0
- package/docs/internal/S10-CONSTRUCTION-HANDOFF-20260917.md +175 -0
- package/docs/internal/S10-GAPS-PLAIN-20260917.md +125 -0
- package/docs/internal/SEMANTIC-ARCHITECTURE-SPEC.md +360 -0
- package/docs/internal/SESSION-FILE-REPAIR-PROTOCOL.md +90 -0
- package/docs/internal/THREE-LAYER-CONTRACT.md +210 -0
- package/docs/internal/TODO-BACKLOG.md +263 -142
- package/docs/internal/TODO-GRAPH.html +715 -0
- package/docs/internal/TODO-GRAPH.html.bak-20260914-v2 +493 -0
- package/docs/internal/TODO-GRAPH.html.bak-20260915-alsfix +710 -0
- package/docs/internal/TODO-GRAPH.html.bak-20260915-p1 +710 -0
- package/docs/internal/TODO-GRAPH.html.bak-20260915-p6a-rev +703 -0
- package/docs/internal/TODO-GRAPH.html.bak-20260915-wshint +710 -0
- package/docs/internal/TODO-GRAPH.html.bak-20260916-batch +715 -0
- package/docs/internal/WB-FORMAT-CONVENTION.md +112 -0
- package/docs/internal/WB-GRAPH-DECISIONS-20260914.md +71 -0
- package/docs/internal/reviews/CLAIM-VERIFICATION-20260914.md +56 -0
- package/docs/internal/reviews/PLAN-gpt6astra-round2-20260914.md +787 -0
- package/docs/internal/reviews/REVIEW-gpt6astra-20260914.md +112 -0
- package/docs/internal/reviews/ROUND3-REVIEW-INTEGRATION-20260914.md +230 -0
- package/docs/prompts/M8-3-enable-verify.md +49 -49
- package/lib/acceptance.js +71 -0
- package/lib/activation-host.js +90 -9
- package/lib/activation-inbox.js +25 -7
- package/lib/board-mode.js +30 -0
- package/lib/client.js +878 -75
- package/lib/context-bridge.js +2 -2
- package/lib/context-host.js +70 -6
- package/lib/engine-identity.js +149 -0
- package/lib/engine-switch.js +247 -0
- package/lib/episodic-store.js +11 -10
- package/lib/evidence-store.js +2 -2
- package/lib/fact-store.js +1 -1
- package/lib/fs-retry.js +46 -0
- package/lib/index.js +1987 -153
- package/lib/intent-clean-safe.js +40 -0
- package/lib/intent-clean.js +12 -16
- package/lib/l0-extract.js +263 -149
- package/lib/l0-index-sync.js +195 -0
- package/lib/l0-index.js +349 -239
- package/lib/ledger-criteria.js +142 -0
- package/lib/m7-index-sync-host.js +65 -4
- package/lib/m7-wire.js +3 -3
- package/lib/memory-anchor.js +56 -1
- package/lib/memory-envelope.js +252 -0
- package/lib/memory-hub.js +14 -4
- package/lib/memory-mutation.js +246 -0
- package/lib/memory-writer.js +204 -24
- package/lib/procedure-observation.js +48 -0
- package/lib/procedure-store.js +34 -17
- package/lib/python-setup.js +1 -1
- package/lib/rerank-host.js +160 -0
- package/lib/rules-layer.js +261 -0
- package/lib/semantic-js.js +15 -0
- package/lib/shadow-retrieval.js +3 -3
- package/lib/state-commit.js +245 -0
- package/lib/subagent-gc.js +4 -8
- package/lib/tier-layer-inject.js +650 -0
- package/lib/tier0-catalog.js +693 -0
- package/lib/water-window.js +263 -186
- package/lib/wb-contract.js +495 -0
- package/lib/wb-sidecar.js +839 -0
- package/lib/ws-overview-rank.js +2 -2
- package/package.json +1 -1
- package/python/m7_embedding_v1.py +5 -5
- package/python/worker_semantic_v1.py +17 -6
- package/python/worker_v1.py +38 -4
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
/** Episode-only rows cannot earn memory-ID evidence. Do not fabricate provenance or gates. */
|
|
4
|
+
export function isObservationOnlyPre(p) {
|
|
5
|
+
if (!p || typeof p !== 'object') return false
|
|
6
|
+
if (p.observationOnly === true) return true
|
|
7
|
+
// Already validated/active, user-authored and deprecated history keeps its existing semantics.
|
|
8
|
+
if (p.origin === 'user' || (p.stage && !['observed', 'candidate'].includes(p.stage))) return false
|
|
9
|
+
return Array.isArray(p.sourceEpisodes) && p.sourceEpisodes.length > 0 &&
|
|
10
|
+
(!p.sourceMemoryIds || p.sourceMemoryIds.length === 0) &&
|
|
11
|
+
(!p.successCriteria || p.successCriteria.length === 0)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Additive metadata migration only; no change to identity, text, risk, evidence or stage. */
|
|
15
|
+
export function normalizeProcedureObservationPre(p) {
|
|
16
|
+
if (!p || p.pinned || p.observationOnly !== undefined || !isObservationOnlyPre(p)) return p
|
|
17
|
+
return { ...p, observationOnly: true }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A title is not an identity. Rich and observation-only candidates must not contaminate each other. */
|
|
21
|
+
export function procedureFingerprintPre(p) {
|
|
22
|
+
return JSON.stringify([p.title, p.origin === 'user' ? 'user' : 'agent', p.riskLevel || 'low',
|
|
23
|
+
isObservationOnlyPre(p), p.steps || [], p.preconditions || [], p.checks || [],
|
|
24
|
+
p.successCriteria || [], p.rollback || []])
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function matchingProcedurePre(rows, candidate) {
|
|
28
|
+
const fingerprint = procedureFingerprintPre(candidate)
|
|
29
|
+
return rows.find(p => p.stage !== 'deprecated' && procedureFingerprintPre(p) === fingerprint)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function mergeProcedureSourcesPre(existing, candidate) {
|
|
33
|
+
// These are the only mergeable fields. In particular, never inherit a lower risk or approval.
|
|
34
|
+
existing.sourceMemoryIds = [...new Set([...(existing.sourceMemoryIds || []), ...(candidate.sourceMemoryIds || [])])]
|
|
35
|
+
existing.sourceEpisodes = [...new Set([...(existing.sourceEpisodes || []), ...(candidate.sourceEpisodes || [])])]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function newProcedureIdentityPre(candidate, now, rows, idPrefix = 'proc_') {
|
|
39
|
+
const used = new Set(rows.map(p => p.procedureId))
|
|
40
|
+
for (let sequence = 0; sequence <= rows.length; sequence++) {
|
|
41
|
+
// 前缀必须与 procedure-store.js 的 PROCEDURE_ID_PREFIX(`proc_`)一致 ——
|
|
42
|
+
// 发布线为 `proc_`,由 tools/release.mjs 反向转换,故此处字面量按 pre 线写。
|
|
43
|
+
const digest = createHash('sha256').update(JSON.stringify(['procedure-v2', procedureFingerprintPre(candidate), now, sequence])).digest('hex')
|
|
44
|
+
const id = idPrefix + digest.slice(0, 32)
|
|
45
|
+
if (!used.has(id)) return id
|
|
46
|
+
}
|
|
47
|
+
throw new Error('procedure-id-exhausted')
|
|
48
|
+
}
|
package/lib/procedure-store.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isObservationOnlyPre, normalizeProcedureObservationPre, matchingProcedurePre, mergeProcedureSourcesPre, newProcedureIdentityPre } from './procedure-observation.js'
|
|
1
2
|
/**
|
|
2
3
|
* M8-2 Procedural Store 纯核心(docs/proactive-associative-memory-system-map.html M-04 Procedural)。
|
|
3
4
|
* 纯内存状态机,零 IO、零依赖(node:crypto 仅作确定性身份);持久化通过可注入 IO 接口,
|
|
@@ -82,6 +83,8 @@ export function validateProcedurePre(p) {
|
|
|
82
83
|
if (!Array.isArray(p.rollback)) q.push('rollback')
|
|
83
84
|
if (typeof p.createdAt !== 'number' || !Number.isFinite(p.createdAt)) q.push('createdAt')
|
|
84
85
|
if (p.requiresApproval !== undefined && typeof p.requiresApproval !== 'boolean') q.push('requiresApproval')
|
|
86
|
+
// issue #30:observationOnly 可选字段,类型必须显式校验(否则脏值会静默通过校验层)。
|
|
87
|
+
if (p.observationOnly !== undefined && typeof p.observationOnly !== 'boolean') q.push('observationOnly')
|
|
85
88
|
// 2026-08-28 Hermes 借鉴字段(可选,向后兼容旧快照):lastUsedAt/pinned/origin
|
|
86
89
|
if (p.lastUsedAt !== undefined && (typeof p.lastUsedAt !== 'number' || !Number.isFinite(p.lastUsedAt))) q.push('lastUsedAt')
|
|
87
90
|
if (p.pinned !== undefined && typeof p.pinned !== 'boolean') q.push('pinned')
|
|
@@ -103,6 +106,7 @@ export function validateProcedureCandidatePre(c) {
|
|
|
103
106
|
if (c.preconditions !== undefined && !Array.isArray(c.preconditions)) p.push('preconditions')
|
|
104
107
|
if (c.checks !== undefined && !Array.isArray(c.checks)) p.push('checks')
|
|
105
108
|
if (c.rollback !== undefined && !Array.isArray(c.rollback)) p.push('rollback')
|
|
109
|
+
if (c.observationOnly !== undefined && typeof c.observationOnly !== 'boolean') p.push('observationOnly')
|
|
106
110
|
if (p.length) return { ok: false, reason: 'invalid:' + p.join(',') }
|
|
107
111
|
return { ok: true, candidate: c }
|
|
108
112
|
}
|
|
@@ -129,7 +133,7 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
129
133
|
const stats = { observed: 0, candidates: 0, validated: 0, activated: 0, deprecated: 0, approvalAsked: 0 }
|
|
130
134
|
|
|
131
135
|
function defaultProcedureId(title, createdAt) {
|
|
132
|
-
const h = createHash('sha256').update(['procedure-
|
|
136
|
+
const h = createHash('sha256').update(['procedure-v1', String(title), String(createdAt)].join('\u0000')).digest('hex')
|
|
133
137
|
return PROCEDURE_ID_PREFIX + h.slice(0, 32)
|
|
134
138
|
}
|
|
135
139
|
|
|
@@ -140,35 +144,42 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
140
144
|
if (!v.ok) return { ok: false, reason: v.reason }
|
|
141
145
|
const c = v.candidate
|
|
142
146
|
const now = nowFn()
|
|
143
|
-
//
|
|
144
|
-
|
|
147
|
+
// issue #30 根因:旧实现按 `title` 去重 —— episode 自动观察出的"仅观察行"通常先建,
|
|
148
|
+
// 之后带真实步骤/成功判据的富候选**同名**时会被合并进观察行,而合并只拷贝证据源字段,
|
|
149
|
+
// 富候选的 steps/successCriteria **整体丢失** ⇒ promote() 永远卡在 `no-success-criteria`,
|
|
150
|
+
// 技能永远无法晋升。修法:① 先把历史行归一化出 observationOnly 标记;
|
|
151
|
+
// ② 用**指纹**(含 steps/criteria/rollback 等全部实质字段)匹配,仅观察行与富候选
|
|
152
|
+
// 指纹不同 ⇒ 各自独立成行,互不污染(标题不是身份)。
|
|
153
|
+
procedures = procedures.map(normalizeProcedureObservationPre)
|
|
154
|
+
const existing = matchingProcedurePre(procedures, c)
|
|
145
155
|
if (existing) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
for (const id of c.sourceMemoryIds || []) if (!seen.has(id)) existing.sourceMemoryIds.push(id)
|
|
149
|
-
const seenEp = new Set(existing.sourceEpisodes)
|
|
150
|
-
for (const id of c.sourceEpisodes || []) if (!seenEp.has(id)) existing.sourceEpisodes.push(id)
|
|
156
|
+
mergeProcedureSourcesPre(existing, c)
|
|
157
|
+
existing.updatedAt = now
|
|
151
158
|
void persist()
|
|
152
159
|
return { ok: true, procedure: existing, merged: true }
|
|
153
160
|
}
|
|
154
161
|
const p = {
|
|
155
|
-
procedureId:
|
|
162
|
+
procedureId: newProcedureIdentityPre(c, now, procedures, PROCEDURE_ID_PREFIX),
|
|
156
163
|
title: c.title,
|
|
157
164
|
stage: 'observed',
|
|
158
165
|
riskLevel: c.riskLevel || 'low',
|
|
159
166
|
requiresApproval: c.riskLevel === 'high' && gates.highRiskRequiresApproval,
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
+
// issue #30:入参数组一律深拷一层,避免调用方后续改动把已入账记录改脏(与 PR#36 口径一致)。
|
|
168
|
+
sourceMemoryIds: [...new Set(c.sourceMemoryIds || [])],
|
|
169
|
+
sourceEpisodes: [...new Set(c.sourceEpisodes || [])],
|
|
170
|
+
preconditions: (c.preconditions || []).slice(),
|
|
171
|
+
steps: c.steps.slice(),
|
|
172
|
+
checks: (c.checks || []).slice(),
|
|
173
|
+
successCriteria: (c.successCriteria || []).slice(),
|
|
174
|
+
rollback: (c.rollback || []).slice(),
|
|
167
175
|
createdAt: now,
|
|
168
176
|
updatedAt: now,
|
|
169
177
|
lastUsedAt: now,
|
|
170
178
|
pinned: false,
|
|
171
179
|
origin: c.origin === 'user' ? 'user' : 'agent',
|
|
180
|
+
// issue #30:纯 episode 观察行显式打标 —— 它没有 successCriteria,永远不该晋升,
|
|
181
|
+
// 也不该与富候选互相污染(此前无标记,只能靠 title 撞车后丢字段)。
|
|
182
|
+
...(isObservationOnlyPre(c) ? { observationOnly: true } : {}),
|
|
172
183
|
evidence: { seen: 0, read: 0, cite: 0, reuse: 0, success: 0, correction: 0, sessions: 0 },
|
|
173
184
|
approved: c.riskLevel !== 'high',
|
|
174
185
|
}
|
|
@@ -256,6 +267,10 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
256
267
|
const p = procedures.find((x) => x.procedureId === procedureId)
|
|
257
268
|
if (!p) return { ok: false, reason: 'not-found' }
|
|
258
269
|
if (p.stage === 'deprecated') return { ok: true, decision: 'keep', reasonCodes: ['deprecated'] }
|
|
270
|
+
// issue #30:纯 episode 观察行没有 successCriteria,本来就无法晋升;
|
|
271
|
+
// 显式短路并给出 `observation-only` 原因码,而不是让它落到下面报 `no-success-criteria`
|
|
272
|
+
// (后者会让使用者误以为"缺判据、补上就能晋升",其实是这行**结构上**不该晋升)。
|
|
273
|
+
if (isObservationOnlyPre(p)) return { ok: true, decision: 'keep', procedure: p, reasonCodes: ['observation-only'] }
|
|
259
274
|
|
|
260
275
|
const ev = p.evidence
|
|
261
276
|
const reason = []
|
|
@@ -377,7 +392,9 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
377
392
|
const v = validateProcedurePre(p)
|
|
378
393
|
if (!v.ok) continue
|
|
379
394
|
if (p._sessions) { p._sessions = new Set(p._sessions); p.evidence.sessions = p._sessions.size }
|
|
380
|
-
|
|
395
|
+
// issue #30:旧快照里没有 observationOnly 字段,恢复时按同一判据补齐
|
|
396
|
+
// (纯增量元数据迁移:不改身份/文本/风险/证据/阶段)。
|
|
397
|
+
procedures.push(normalizeProcedureObservationPre(v.procedure))
|
|
381
398
|
}
|
|
382
399
|
return { ok: true, restored: procedures.length }
|
|
383
400
|
}
|
package/lib/python-setup.js
CHANGED
|
@@ -51,7 +51,7 @@ const PIP_DEPS_GPU_EXTRA = ['onnxruntime-gpu']
|
|
|
51
51
|
* 历史上探针多要一个 torch,而 PIP_DEPS_CPU 从不装 torch → depsOk 恒 false,"全部就绪"永不出现。 */
|
|
52
52
|
const DEPS_PROBE = 'import transformers, onnxruntime, numpy; print("deps-ok")'
|
|
53
53
|
|
|
54
|
-
/** worker 侧 load_embedder 的档位名(m7_embedding_v1.PROVIDER_REAL_INT8);发布构建会把 -
|
|
54
|
+
/** worker 侧 load_embedder 的档位名(m7_embedding_v1.PROVIDER_REAL_INT8);发布构建会把 -v1 折成 -v1。 */
|
|
55
55
|
const PROVIDER_ID_INT8 = 'bge-m3-onnx-int8-v1'
|
|
56
56
|
/** dense 维度(BGE-M3 = 1024),写进 embedding-config.json 的 dimension。 */
|
|
57
57
|
const EMBED_DIMENSION = 1024
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rerank-host-pre —— P4 精排多级档位 + 有界异步窗口(2026-09-16, rerank_host_v1)。
|
|
3
|
+
*
|
|
4
|
+
* 权威依据: MASTER-PLAN-3.0.md §Phase 4 (H2 按实测重写) + TODO-GRAPH V2-P4 卡。
|
|
5
|
+
* 实测推翻方案假设(留痕): bge-reranker-v2-m3 P95 37.4 秒(50 对/题, RSS 3.84GB);
|
|
6
|
+
* qwen3-reranker-0.6b P95 8.8 秒(小样本探针 10 对/题, RSS 4.95GB, 不可与 bge 直接比);
|
|
7
|
+
* 收益真实: recall@1 0.739 → 0.898(bge); cross-encoder 模型已下载; 真实缺口 = GPU 版 torch
|
|
8
|
+
* (torch 2.13.0+cpu)与快档量化产物。⇒ 精排不得同步等待, 只能走**有界异步窗口**。
|
|
9
|
+
*
|
|
10
|
+
* 设计(卡内 v2 修正, 逐条对应):
|
|
11
|
+
* - 多级档位(用户裁定): 'off' | 'fast'(int8+CPU, 未就绪=不可用) | 'enthusiast'(完整模型+GPU)。
|
|
12
|
+
* 档位只决定"是否允许排精排任务", 一期本地无 GPU torch / 无量化产物 ⇒ fast/enthusiast 均
|
|
13
|
+
* fail closed 降级为粗排(绝不阻塞前台、绝不发出模型请求)。
|
|
14
|
+
* - 一分钟 = 有界后台窗口: 从**入队时**起算含排队与计算, **到期不续命**; 本轮立即用现有排序;
|
|
15
|
+
* 后台完成只进有界结果缓存, 不改本轮 envelope、不再次 emit; 下一请求重新生成 requestKey,
|
|
16
|
+
* 仅 inputKey 完全匹配才复用, 且重跑当轮检查; 没有下一轮就过期, 不主动制造模型请求。
|
|
17
|
+
* - 运行隔离: 精排必须走独立懒加载 worker 角色(注入 opts.rerankFn), 不得占用稠密/索引 worker;
|
|
18
|
+
* 全机最多一个精排任务在跑(busy 门), 忙时继续粗排。
|
|
19
|
+
* - 资源门 T5-5R: 任务超清理宽限未被取消/完成 ⇒ 调 opts.killFn 终止并释放; 期间稠密查询照常。
|
|
20
|
+
* - T5-6R: 本模块不产出任何"异步注入收益"口径 — 结果只进缓存; 零复用就是零复用。
|
|
21
|
+
*
|
|
22
|
+
* 纯主机侧状态机, 模型调用经 opts.rerankFn 注入(测试可控), 零 fs/零网络。
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export const RERANK_HOST_VERSION = 'rerank_host_v1'
|
|
26
|
+
|
|
27
|
+
/** 有界异步窗口: 从入队起算(毫秒), 含排队与计算; 到期不续命。 */
|
|
28
|
+
export const RERANK_WINDOW_MS_V1 = 60000
|
|
29
|
+
/** 登记的清理宽限: 超窗后仍允许 killFn 在此宽限内终止进程。 */
|
|
30
|
+
export const RERANK_KILL_GRACE_MS_V1 = 5000
|
|
31
|
+
/** 缓存上限(条): 有界结果缓存, LRU 逐出。 */
|
|
32
|
+
export const RERANK_CACHE_MAX_V1 = 16
|
|
33
|
+
|
|
34
|
+
/** 档位解析: 'off'/'fast'/'enthusiast' 之外一律按 off(fail closed)。 */
|
|
35
|
+
export function resolveRerankTierPre(raw) {
|
|
36
|
+
const s = String(raw || '').toLowerCase()
|
|
37
|
+
if (s === 'fast' || s === 'enthusiast') return { tier: s, active: false, reason: 'tier-not-provisioned' }
|
|
38
|
+
if (s === 'off') return { tier: 'off', active: false, reason: 'tier-off' }
|
|
39
|
+
return { tier: 'off', active: false, reason: 'invalid-tier' }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* inputKey: 精排复用的完全匹配键 — 查询 + 排序输入 + miv 的规范化哈希。
|
|
44
|
+
* 任一身份字段变化 ⇒ 键不同 ⇒ 不复用(T5-3R)。
|
|
45
|
+
*/
|
|
46
|
+
export function computeRerankInputKeyPre({ query, orderedIds, memoryIndexVersion }) {
|
|
47
|
+
return sha256Hex(JSON.stringify([String(query || ''), JSON.stringify(Array.isArray(orderedIds) ? orderedIds.map(String) : []), String(memoryIndexVersion || '')]))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sha256Hex(s) {
|
|
51
|
+
// 纯 JS sha256 避免引入 node:crypto 依赖面 — 复用 djb2 式双哈希足够做键(非安全用途),
|
|
52
|
+
// 但为确定性可审计, 用 FNV-1a 64 位两次盐化。
|
|
53
|
+
let h1 = 0x811c9dc5, h2 = 0x811c9dc5
|
|
54
|
+
for (let i = 0; i < s.length; i++) {
|
|
55
|
+
const c = s.charCodeAt(i)
|
|
56
|
+
h1 = ((h1 ^ c) * 0x01000193) >>> 0
|
|
57
|
+
h2 = ((h2 ^ (c + i)) * 0x01000193) >>> 0
|
|
58
|
+
}
|
|
59
|
+
return h1.toString(16).padStart(8, '0') + h2.toString(16).padStart(8, '0')
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* 创建精排主机(每引擎一份)。
|
|
64
|
+
* opts: {
|
|
65
|
+
* tier: 'off'|'fast'|'enthusiast',
|
|
66
|
+
* now?: () => ms(测试可注入),
|
|
67
|
+
* rerankFn?: async ({query, ordered}) => number[] // 独立 worker 角色; 懒加载由调用方注入
|
|
68
|
+
* killFn?: () => void // T5-5R: 终止精排进程并释放
|
|
69
|
+
* }
|
|
70
|
+
*/
|
|
71
|
+
export function createRerankHostPre(opts = {}) {
|
|
72
|
+
const now = typeof opts.now === 'function' ? opts.now : () => Date.now()
|
|
73
|
+
const resolved = resolveRerankTierPre(opts.tier)
|
|
74
|
+
const stats = { enqueued: 0, reused: 0, expiredDrops: 0, killed: 0, completed: 0, busyRejects: 0, tierRejects: 0 }
|
|
75
|
+
/** Map<inputKey, {at, scores}> — 有界结果缓存(只缓存, 不推送)。 */
|
|
76
|
+
const cache = new Map()
|
|
77
|
+
let running = null // {inputKey, startedAt, done}
|
|
78
|
+
|
|
79
|
+
function pruneExpired() {
|
|
80
|
+
const t = now()
|
|
81
|
+
for (const [k, v] of cache) {
|
|
82
|
+
if (t - v.at > RERANK_WINDOW_MS_V1) { cache.delete(k); stats.expiredDrops++ }
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 请求一次精排机会(每轮 recall 调用一次)。
|
|
88
|
+
* 返回 {mode, scores?, reason?}:
|
|
89
|
+
* mode='reuse' — 缓存命中(窗口内 + inputKey 完全匹配); 调用方把 scores 并进本轮展示。
|
|
90
|
+
* mode='skip' — 档位关/未就绪/无 worker/忙/无下一轮价值 ⇒ 本轮粗排, 不发模型请求。
|
|
91
|
+
* mode='accepted' — 已入队(有界异步窗口起算); 本轮立即返回(粗排序不变)。
|
|
92
|
+
* **本轮同步路径绝不等待精排**(T5-3R: 前台先返回, 后台完成只进缓存)。
|
|
93
|
+
*/
|
|
94
|
+
function offerRerankPre({ query, orderedIds, memoryIndexVersion, requestKey }) {
|
|
95
|
+
pruneExpired()
|
|
96
|
+
const tier = resolveRerankTierPre(opts.tier)
|
|
97
|
+
if (tier.tier === 'off') { stats.tierRejects++; return { mode: 'skip', reason: 'tier-off' } }
|
|
98
|
+
// fast/enthusiast 的可用性由"是否注入了独立 worker"决定(运行隔离: rerankFn 即懒加载 worker 角色)。
|
|
99
|
+
// 环境未供给(GPU torch 缺/量化产物缺) ⇒ 调用方不注入 rerankFn ⇒ 这里 fail closed 走粗排。
|
|
100
|
+
if (typeof opts.rerankFn !== 'function') { stats.tierRejects++; return { mode: 'skip', reason: 'no-worker' } }
|
|
101
|
+
const inputKey = computeRerankInputKeyPre({ query, orderedIds, memoryIndexVersion })
|
|
102
|
+
const hit = cache.get(inputKey)
|
|
103
|
+
if (hit && now() - hit.at <= RERANK_WINDOW_MS_V1) {
|
|
104
|
+
// T5-4R: 复用前调用方必须重跑当轮状态/fv2/冷却/预算检查 — 本模块只担保 inputKey 完全匹配。
|
|
105
|
+
stats.reused++
|
|
106
|
+
return { mode: 'reuse', scores: hit.scores, inputKey }
|
|
107
|
+
}
|
|
108
|
+
if (running) { stats.busyRejects++; return { mode: 'skip', reason: 'busy', inputKey } }
|
|
109
|
+
// 入队: 一期全机最多一个精排任务; 有界窗口从入队起算, 到期不续命。
|
|
110
|
+
stats.enqueued++
|
|
111
|
+
const startedAt = now()
|
|
112
|
+
const job = { inputKey, startedAt, requestKey: String(requestKey || '') }
|
|
113
|
+
running = job
|
|
114
|
+
Promise.resolve()
|
|
115
|
+
.then(() => opts.rerankFn({ query, ordered: Array.isArray(orderedIds) ? orderedIds.map(String) : [] }))
|
|
116
|
+
.then((scores) => {
|
|
117
|
+
// 到期不续命: 完成时已超窗 ⇒ 丢弃(没有下一轮就过期, 不制造请求)。
|
|
118
|
+
if (now() - startedAt > RERANK_WINDOW_MS_V1) { stats.expiredDrops++; return }
|
|
119
|
+
// 缓存条目记入队时刻(卡内口径: 窗口从入队起算, 不是完成时刻 — 否则慢计算等于续命)。
|
|
120
|
+
cache.set(inputKey, { at: startedAt, scores: Array.isArray(scores) ? scores : [] })
|
|
121
|
+
if (cache.size > RERANK_CACHE_MAX_V1) {
|
|
122
|
+
const oldest = [...cache.entries()].sort((a, b) => a[1].at - b[1].at)[0]
|
|
123
|
+
if (oldest) cache.delete(oldest[0])
|
|
124
|
+
}
|
|
125
|
+
stats.completed++
|
|
126
|
+
})
|
|
127
|
+
.catch(() => { /* fail soft: 精排失败不影响粗排 */ })
|
|
128
|
+
.finally(() => { if (running === job) running = null })
|
|
129
|
+
return { mode: 'accepted', inputKey }
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* T5-5R: 精排忽略取消/卡死 ⇒ 调用方在登记的清理宽限内调用; 终止进程并释放占用。
|
|
134
|
+
* 返回 {killed:boolean}。稠密查询与本主机其余路径不受影响。
|
|
135
|
+
*/
|
|
136
|
+
function killRunningRerankPre() {
|
|
137
|
+
if (!running) return { killed: false }
|
|
138
|
+
running = null
|
|
139
|
+
stats.killed++
|
|
140
|
+
try { if (typeof opts.killFn === 'function') opts.killFn() } catch (_) {}
|
|
141
|
+
return { killed: true }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** 是否存在超窗未完成的运行任务(调用方据此在宽限内 kill)。 */
|
|
145
|
+
function isRunningBeyondWindowPre() {
|
|
146
|
+
return !!(running && now() - running.startedAt > RERANK_WINDOW_MS_V1 + RERANK_KILL_GRACE_MS_V1)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function debugView() {
|
|
150
|
+
pruneExpired()
|
|
151
|
+
return {
|
|
152
|
+
version: RERANK_HOST_VERSION, tier: resolved.tier, active: resolved.active,
|
|
153
|
+
running: running ? { inputKey: running.inputKey, elapsedMs: now() - running.startedAt, beyondWindow: isRunningBeyondWindowPre() } : null,
|
|
154
|
+
cacheSize: cache.size, stats,
|
|
155
|
+
windowMs: RERANK_WINDOW_MS_V1, killGraceMs: RERANK_KILL_GRACE_MS_V1,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { offerRerankPre, killRunningRerankPre, isRunningBeyondWindowPre, debugView }
|
|
160
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 规则层抽取(rules_layer_v1)—— P6A 的"分类"部分(Phase 6 拆分中的 6A)。
|
|
3
|
+
*
|
|
4
|
+
* 2026-09-14 建立(P0 第五步)。**它解决的是用户最痛的病之一**:
|
|
5
|
+
* 注入开场白原来把所有记忆统一降格为「只是背景事实与规则参考」(`lib/index.js` 的
|
|
6
|
+
* `DEFAULT_PROMPT_LAYERS.snapshotHead`),**规则与资料混在一句措辞里** ⇒ 模型注意力不落在规矩上。
|
|
7
|
+
* 用户原话:「这个 just for reference 说得太轻了,模型注意力没有在这上面。」
|
|
8
|
+
*
|
|
9
|
+
* **本模块只做一件事**:从记忆文本里**认出规则类条目**,把它们与参考类分开。
|
|
10
|
+
* 措辞分层(不同引导语)与节奏(每轮在场)由调用方负责。
|
|
11
|
+
*
|
|
12
|
+
* **真源纪律(v2 修正,必须遵守)**:用户级规则=**既有用户级记忆文件**(`~/.dsh/memory/MEMORY.md`)
|
|
13
|
+
* 里的类型化规则,**不是新增 `RULES.md`**。`ROUND3 §2.3` 记录了我方曾在两处写了两个真源,
|
|
14
|
+
* 被 GPT 抓出。若将来真要独立真源,必须补迁移/去重/旧模式读取/回滚四类测试。
|
|
15
|
+
*
|
|
16
|
+
* **两级判定**(T7-4 的"双保险",v2 修正后的形态):
|
|
17
|
+
* ① **结构化前缀**(高置信):`【用户硬性规则…】` 这类显式标记 —— 这是既有记忆的真实形态
|
|
18
|
+
* (实测:`~/.dsh/memory/MEMORY.md` 首条即 `【用户硬性规则 - 文件编码】`)。
|
|
19
|
+
* ② **约束语汇**(中置信):`严禁` / `绝不` / `必须` / `不得` 等。
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ **v2 明确纠正过我方原设计**:不能"命中关键词就无条件升级为规则"。
|
|
22
|
+
* 反例(GPT 给):`"文档写着必须重启"`、`"曾经要求必须 X 但已取消"` —— 这类是**描述**不是**要求**。
|
|
23
|
+
* 故本模块对 ② 类的结果标注 `confidence:'medium'` 并**保留原文里的引用/历史语境标记**
|
|
24
|
+
* (`引文` / `已取消` / `据文档` 等),由调用方决定是否只作为「待确认候选」。
|
|
25
|
+
* 本模块**不把中置信项冒充高置信**——那正是 T7-4 要防的误报。
|
|
26
|
+
*
|
|
27
|
+
* **依赖边界(如实标注,不宣称已保证)**:本模块保证的是"规则段被**注入函数**产出且不受节流与裁剪"。
|
|
28
|
+
* "模型**真的收到了**"取决于宿主最终请求 messages 的确认(`MASTER-PLAN-3.0.md §7` 的 **U6**,
|
|
29
|
+
* 尚未具备)⇒ **不得**据此宣称"规则的遵守问题已解决"(T7-7 的纪律)。
|
|
30
|
+
*
|
|
31
|
+
* S9 合规:零 IO、零依赖、纯函数、无网络/无 LLM/无子进程/无 await。UTF-8 无 BOM。
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
export const RULES_LAYER_VERSION = 'rules_layer_v1'
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* ★P6B(2026-09-15 · 用户裁定开工):日志行内 kind 标记 —— 规则分类的**持久化**落点。
|
|
38
|
+
*
|
|
39
|
+
* 权威依据:`TODO-GRAPH.html` V2-P6B 卡 points[1]("分类时机:不做独立 LLM 调用,
|
|
40
|
+
* memory_log 本来就在本轮内由模型直接写,顺手打 kind 标记=零额外成本")+ crit[1](T7-5)。
|
|
41
|
+
*
|
|
42
|
+
* **形态选行内标记** `- HH:MM [kind:rule] 内容`:日志是 append-only 纯文本,行内标记
|
|
43
|
+
* 可被本模块的条目切分生态直接消费,且重启/迁移不丢(不依赖任何 sidecar)。
|
|
44
|
+
* kind 作**可选参数**(缺省 fact):不传时写入行与旧版逐字节一致(新旧并存 + 开关回退纪律)。
|
|
45
|
+
*/
|
|
46
|
+
export const LOG_KINDS_V1 = Object.freeze(['rule', 'preference', 'fact', 'todo'])
|
|
47
|
+
|
|
48
|
+
/** 行内标记的正则形态(渲染/解析两侧共用,不各写一套)。 */
|
|
49
|
+
export const LOG_KIND_TAG_RE_V1 = /\s*\[kind:(rule|preference|fact|todo)\]\s*/
|
|
50
|
+
|
|
51
|
+
/** 结构化前缀(高置信):既有用户级记忆的真实形态。 */
|
|
52
|
+
export const RULE_MARKERS_V1 = Object.freeze([
|
|
53
|
+
'【用户硬性规则', '【硬性规则', '【规则】', '【必须遵守', '【禁止', '【用户规则',
|
|
54
|
+
])
|
|
55
|
+
|
|
56
|
+
/** 约束语汇(中置信)—— 单靠它**不足以**判定为规则(见文件头 T7-4 说明)。 */
|
|
57
|
+
export const RULE_CONSTRAINT_WORDS_V1 = Object.freeze([
|
|
58
|
+
'严禁', '绝不', '必须', '不得', '禁止', '务必', '一律', '永远不', '不要再', '不准',
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 引用/历史语境标记:出现这些 ⇒ 该段说的是"别处这么写"或"曾经如此",
|
|
63
|
+
* **不是**当前有效的规则要求 ⇒ 降级为 `candidate`(待确认候选),不得自动取得不可裁地位。
|
|
64
|
+
* 这是 v2 §3.3(Q7 第 2 条)给的反例在代码里的落点。
|
|
65
|
+
*/
|
|
66
|
+
export const RULE_DESCRIPTION_MARKERS_V1 = Object.freeze([
|
|
67
|
+
'据文档', '文档写着', '文档说', '曾经', '已取消', '已废弃', '不再要求', '原要求', '旧规则', '据说',
|
|
68
|
+
])
|
|
69
|
+
|
|
70
|
+
/** 条目切分锚点:与 `l0-extract.js` 的记忆锚点完全一致(不另立一套)。 */
|
|
71
|
+
const MEM_ANCHOR_LINE_RE = /^<!--\s*memory:(mem_[0-9a-f]{32})\s*-->$/
|
|
72
|
+
|
|
73
|
+
/** 条目内的日期小节(`## 2026-08-14`)——既有记忆文件的实际结构。 */
|
|
74
|
+
const DATE_SECTION_RE = /^##\s*\d{4}-\d{2}-\d{2}\s*$/
|
|
75
|
+
|
|
76
|
+
const clean = (s) => String(s == null ? '' : s)
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 把记忆文本按锚点切成条目(保持既有文件结构语义:无锚点的前置内容归入首条)。
|
|
80
|
+
*
|
|
81
|
+
* @param {string} text 用户级/工作区级记忆文件全文
|
|
82
|
+
* @returns {Array<{id:string, anchorLine:number, text:string, lines:string[]}>}
|
|
83
|
+
*/
|
|
84
|
+
export function splitMemoryEntriesPre(text) {
|
|
85
|
+
const src = clean(text).replace(/^\uFEFF/, '')
|
|
86
|
+
if (!src.trim()) return []
|
|
87
|
+
const lines = src.split('\n')
|
|
88
|
+
const out = []
|
|
89
|
+
let cur = null
|
|
90
|
+
for (let i = 0; i < lines.length; i++) {
|
|
91
|
+
const m = MEM_ANCHOR_LINE_RE.exec(lines[i].replace(/\r$/, ''))
|
|
92
|
+
if (m) {
|
|
93
|
+
if (cur) out.push(cur)
|
|
94
|
+
cur = { id: m[1], anchorLine: i + 1, lines: [] }
|
|
95
|
+
continue
|
|
96
|
+
}
|
|
97
|
+
if (cur) cur.lines.push(lines[i])
|
|
98
|
+
}
|
|
99
|
+
if (cur) out.push(cur)
|
|
100
|
+
if (!out.length) {
|
|
101
|
+
// 无锚点文件(旧格式)⇒ 整篇算一条,不丢内容
|
|
102
|
+
return [{ id: '', anchorLine: 0, lines, text: src.trim() }]
|
|
103
|
+
}
|
|
104
|
+
return out.map((e) => ({ id: e.id, anchorLine: e.anchorLine, lines: e.lines, text: e.lines.join('\n').trim() }))
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 单条文本的规则分类(返回置信度与理由,不抛异常)。 */
|
|
108
|
+
function classifyText(rawText) {
|
|
109
|
+
const text = clean(rawText)
|
|
110
|
+
if (!text.trim()) return { kind: 'reference', confidence: 'none', reasons: ['空内容'] }
|
|
111
|
+
const marker = RULE_MARKERS_V1.find((k) => text.includes(k))
|
|
112
|
+
if (marker) {
|
|
113
|
+
return { kind: 'rule', confidence: 'high', reasons: ['结构化前缀 ' + marker] }
|
|
114
|
+
}
|
|
115
|
+
const words = RULE_CONSTRAINT_WORDS_V1.filter((w) => text.includes(w))
|
|
116
|
+
if (!words.length) return { kind: 'reference', confidence: 'none', reasons: ['无规则标记与约束语汇'] }
|
|
117
|
+
const desc = RULE_DESCRIPTION_MARKERS_V1.filter((w) => text.includes(w))
|
|
118
|
+
if (desc.length) {
|
|
119
|
+
// ★ v2 修正的落点:含"必须"但是**描述/引文**,不得自动升级为规则
|
|
120
|
+
return {
|
|
121
|
+
kind: 'candidate', confidence: 'low',
|
|
122
|
+
reasons: ['约束语汇(' + words.slice(0, 3).join('/') + ')但含引用/历史语境(' + desc.slice(0, 3).join('/') + ')⇒ 仅作待确认候选,不自动取得规则地位'],
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return { kind: 'rule', confidence: 'medium', reasons: ['约束语汇 ' + words.slice(0, 3).join('/')] }
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* 从记忆文本里抽出规则层(分项,供注入侧分层措辞与"不参与裁剪")。
|
|
130
|
+
*
|
|
131
|
+
* @param {object} input
|
|
132
|
+
* @param {string} [input.userText] 用户级记忆(跨工作区恒定;**规则真源**,见文件头)
|
|
133
|
+
* @param {string} [input.notesText] 工作区级项目笔记(第二层)
|
|
134
|
+
* @param {string} [input.rulesLayeringMode] 'off' | 'self' | 'none'
|
|
135
|
+
* - `'off'`:**默认**。返回空规则层(调用方回落旧行为:统一措辞 + 原有节奏)。这就是"新旧并存 + 开关回退"。
|
|
136
|
+
* - `'self'`:只看**当前 agent 自己写的** user 层(隔离子代理/外部导入的规则,防串线)。
|
|
137
|
+
* - `'none'`:不看任何 user 层(只保留工作区级)。
|
|
138
|
+
* @param {string} [input.ownerSessionId] `rulesLayeringMode='self'` 时用于判定"是不是我自己写的"
|
|
139
|
+
* @returns {{version:string, mode:string, enabled:boolean, text:string, rules:Array, candidates:Array,
|
|
140
|
+
* references:Array, counts:object, chars:{rules:number,candidates:number,total:number}}}
|
|
141
|
+
*/
|
|
142
|
+
export function extractRulesLayerPre(input = {}) {
|
|
143
|
+
const o = input && typeof input === 'object' ? input : {}
|
|
144
|
+
const mode = clampMode(o.rulesLayeringMode)
|
|
145
|
+
const empty = {
|
|
146
|
+
version: RULES_LAYER_VERSION, mode, enabled: false, text: '', rules: [], candidates: [], references: [],
|
|
147
|
+
counts: { entries: 0, rules: 0, candidates: 0, references: 0 }, chars: { rules: 0, candidates: 0, total: 0 },
|
|
148
|
+
}
|
|
149
|
+
if (mode === null) return empty
|
|
150
|
+
|
|
151
|
+
const sources = []
|
|
152
|
+
const pushAll = (layer, text) => {
|
|
153
|
+
for (const e of splitMemoryEntriesPre(text)) sources.push({ layer, ...e })
|
|
154
|
+
}
|
|
155
|
+
// 模式语义(与文件头一致,别写反):
|
|
156
|
+
// 'self' ⇒ 看用户级(跨工作区恒定)+ 工作区级
|
|
157
|
+
// 'none' ⇒ **不看任何用户级**,只看工作区级(用于"工作区规则覆盖用户规则"的场景)
|
|
158
|
+
if (mode !== 'none') pushAll('user', o.userText)
|
|
159
|
+
pushAll('project', o.notesText)
|
|
160
|
+
|
|
161
|
+
const rules = []
|
|
162
|
+
const candidates = []
|
|
163
|
+
const references = []
|
|
164
|
+
for (const e of sources) {
|
|
165
|
+
const c = classifyText(e.text)
|
|
166
|
+
const rec = { layer: e.layer, id: e.id, anchorLine: e.anchorLine, kind: c.kind, confidence: c.confidence, reasons: c.reasons, text: e.text }
|
|
167
|
+
if (c.kind === 'rule') rules.push(rec)
|
|
168
|
+
else if (c.kind === 'candidate') candidates.push(rec)
|
|
169
|
+
else references.push(rec)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 规则层文本:只含规则条目(高/中置信),逐条一行摘要,**不掺参考类**
|
|
173
|
+
const renderLine = (r) => '- ' + ruleSummaryPre(r.text)
|
|
174
|
+
const parts = []
|
|
175
|
+
if (rules.length) parts.push(rules.map(renderLine).join('\n'))
|
|
176
|
+
const text = parts.join('\n')
|
|
177
|
+
return {
|
|
178
|
+
version: RULES_LAYER_VERSION, mode, enabled: true, text, rules, candidates, references,
|
|
179
|
+
counts: { entries: sources.length, rules: rules.length, candidates: candidates.length, references: references.length },
|
|
180
|
+
chars: { rules: text.length, candidates: candidates.reduce((a, r) => a + ruleSummaryPre(r.text).length + 2, 0), total: text.length },
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** 模式解析:`null` 表示"未启用"(调用方沿用旧行为)。 */
|
|
185
|
+
function clampMode(v) {
|
|
186
|
+
const s = clean(v).trim().toLowerCase()
|
|
187
|
+
if (!s || s === 'off' || s === 'false' || s === '0') return null
|
|
188
|
+
if (s === 'self' || s === 'none' || s === 'all') return s
|
|
189
|
+
return null // 非法值一律当"未启用"(fail-soft,不因配置写错而改变注入)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** 取第一条非空行并裁剪(规则条目往往首行就是规则名,够用且省 token)。 */
|
|
193
|
+
export function firstLine(text, max = 200) {
|
|
194
|
+
const lines = clean(text).split('\n').map((l) => l.trim()).filter(Boolean)
|
|
195
|
+
const first = lines[0] || ''
|
|
196
|
+
return first.length > max ? first.slice(0, Math.max(1, max - 1)) + '…' : first
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* 规则条目的**摘要行**(渲染用):跳过日期小节标题(`## 2026-08-14`)与纯标点行,
|
|
201
|
+
* 取第一条实质内容。
|
|
202
|
+
*
|
|
203
|
+
* **为什么需要它**(实测踩坑):既有记忆文件的条目结构是「锚点 → `## 日期` → 正文」,
|
|
204
|
+
* 直接用 `firstLine` 会渲染成 `- ## 2026-08-14` —— 规则内容全丢,只剩日期,
|
|
205
|
+
* 而且**看起来还挺正常**(这条断言首跑就红在"连续 6 轮规则都在"上,因为摘要里没有规则文本)。
|
|
206
|
+
*
|
|
207
|
+
* ★P6B:`[kind:x]` 行内标记在此**剥离**(标记只服务盘上审计与分类持久化,
|
|
208
|
+
* 注入摘要保持干净);时间戳 `HH:MM` 一并剥离(对规则语义是噪声)。
|
|
209
|
+
*/
|
|
210
|
+
export function ruleSummaryPre(text, max = 200) {
|
|
211
|
+
const lines = clean(text).split('\n').map((l) => l.trim()).filter(Boolean)
|
|
212
|
+
for (const l of lines) {
|
|
213
|
+
if (DATE_SECTION_RE.test(l)) continue
|
|
214
|
+
if (/^<!--/.test(l)) continue
|
|
215
|
+
if (/^[-*+]\s*$/.test(l)) continue
|
|
216
|
+
// 去掉列表前缀再返回,保持一条一行
|
|
217
|
+
let body = l.replace(/^[-*+]\s+/, '')
|
|
218
|
+
// ★P6B:剥行内 kind 标记与前置时间戳(只影响渲染,不回写盘上原文)
|
|
219
|
+
body = body.replace(LOG_KIND_TAG_RE_V1, ' ').trim()
|
|
220
|
+
body = body.replace(/^\d{1,2}:\d{2}\s+/, '').trim()
|
|
221
|
+
return body.length > max ? body.slice(0, Math.max(1, max - 1)) + '…' : body
|
|
222
|
+
}
|
|
223
|
+
return ''
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* 规则段渲染(**供注入侧直接使用**):标题 + 引导语 + 逐条规则。
|
|
228
|
+
*
|
|
229
|
+
* 措辞分层的关键(T7-3):**规则类用约束语**("必须遵守"),**参考类保留"参考"语义**。
|
|
230
|
+
* 两段引导语必须**不相同** —— 断言直接锁这一点,防止有人改回统一措辞。
|
|
231
|
+
*
|
|
232
|
+
* @param {object} rules `extractRulesLayerPre` 的返回值
|
|
233
|
+
* @param {object} [opts]
|
|
234
|
+
* @param {string} [opts.title] 段标题(默认常量)
|
|
235
|
+
* @param {string} [opts.guide] 引导语(默认常量)
|
|
236
|
+
* @returns {{text:string, chars:number, guide:string}}
|
|
237
|
+
*/
|
|
238
|
+
export function renderRulesSectionPre(rules, opts = {}) {
|
|
239
|
+
const r = rules && typeof rules === 'object' ? rules : {}
|
|
240
|
+
const list = Array.isArray(r.rules) ? r.rules : []
|
|
241
|
+
if (!list.length) return { text: '', chars: 0, guide: '' }
|
|
242
|
+
const title = clean((opts && opts.title) || RULES_SECTION_TITLE_V1)
|
|
243
|
+
const guide = clean((opts && opts.guide) || RULES_SECTION_GUIDE_V1)
|
|
244
|
+
const text = '\n' + title + '\n' + guide + '\n' + list.map((x) => '- ' + ruleSummaryPre(x.text)).join('\n')
|
|
245
|
+
return { text, chars: text.length, guide }
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* 规则段标题(T7-3:与参考段的措辞**必须不同**;断言直接比对两者)。
|
|
250
|
+
* 用户可通过 `promptLayerOverrides.snapshotRulesTitle` 覆盖。
|
|
251
|
+
*/
|
|
252
|
+
export const RULES_SECTION_TITLE_V1 = '[规则 — 用户级硬性约束 · 必须遵守]'
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* 规则段引导语:**约束语**("以下为必须遵守的约束"),不是"只是参考"。
|
|
256
|
+
* 这句话就是 P6A「措辞」要修的病:旧开场白把规矩与资料统一降格为「只是背景事实与规则参考」。
|
|
257
|
+
*/
|
|
258
|
+
export const RULES_SECTION_GUIDE_V1 = '以下条目是用户明确要求长期遵守的约束,不是可选背景。凡与其它内容冲突,以本节为准;无法满足时必须显式说明。'
|
|
259
|
+
|
|
260
|
+
/** 参考段引导语(保留"参考"语义,与规则段措辞不同)。 */
|
|
261
|
+
export const REFERENCE_SECTION_GUIDE_V1 = '以下为背景资料与历史记录,供参考与检索定位;与上面的规则冲突时不适用。'
|
package/lib/semantic-js.js
CHANGED
|
@@ -342,6 +342,21 @@ export function createJsSemanticEnginePre(opts = {}) {
|
|
|
342
342
|
return null // fail closed:词法回退由调用方自然发生
|
|
343
343
|
}
|
|
344
344
|
},
|
|
345
|
+
/**
|
|
346
|
+
* 2026-09-14(三层契约 C3):暴露**批量段落嵌入**通道 —— L0 向量索引接线
|
|
347
|
+
* (`lib/l0-index-sync.js`)需要把 L0 摘要本身变成向量,而 rank() 只返回
|
|
348
|
+
* 「查询 vs 内部缓存索引」的余弦分,**不产出向量**(此前宿主没有任何拿向量的口子,
|
|
349
|
+
* 这是 `createL0IndexPre` 一直零引用、无法接线的原因之一)。
|
|
350
|
+
*
|
|
351
|
+
* 语义:裸文本数组 → `Float32Array[]`(e5 口径为 384 维已归一化;前缀 `passage: ` 由引擎内部加,
|
|
352
|
+
* 与 rank 的 `query: ` 前缀对称,调用方传**裸文本**)。
|
|
353
|
+
* 失败语义:模型资产缺失/加载失败 → 抛错(**不吞**)—— 由调用方 fail-soft(索引侧只 diag 一行,
|
|
354
|
+
* 不阻塞任何检索路径)。本方法不改任何内部状态(不缓存、不动 idx),纯计算。
|
|
355
|
+
*/
|
|
356
|
+
async embedPassages(texts) {
|
|
357
|
+
const t = await ensureTier()
|
|
358
|
+
return t.embedPassages(Array.isArray(texts) ? texts : [])
|
|
359
|
+
},
|
|
345
360
|
_resetForTest() { tier = null; tierPromise = null; degraded = ''; idx = { miv: null, entries: [] }; rebuilding = null },
|
|
346
361
|
}
|
|
347
362
|
}
|
package/lib/shadow-retrieval.js
CHANGED
|
@@ -402,14 +402,14 @@ const MEMORY_ID_STRICT = /^mem_[0-9a-f]{32}$/
|
|
|
402
402
|
|
|
403
403
|
/** §11 retrievalId(确定性,可重放;非长期内容身份)。 */
|
|
404
404
|
export function buildRetrievalId(sessionId, contextVersion, triggerSegmentId, memoryIndexVersion) {
|
|
405
|
-
const sessionIdHash = first32(sha256Str('retrieval-
|
|
406
|
-
const parts = ['retrieval-
|
|
405
|
+
const sessionIdHash = first32(sha256Str('retrieval-v1\u0000' + sessionId))
|
|
406
|
+
const parts = ['retrieval-v1', sessionIdHash, contextVersion, triggerSegmentId, memoryIndexVersion, GATE_POLICY_VERSION, LEXICAL_POLICY_VERSION]
|
|
407
407
|
return RETRIEVAL_PREFIX + first32(sha256Str(JSON.stringify(parts)))
|
|
408
408
|
}
|
|
409
409
|
|
|
410
410
|
/** §11 candidateId:同 memoryId 内容变化后 candidateId 因版本/digest 改变。 */
|
|
411
411
|
export function buildCandidateId(retrievalId, memoryId, sourceEpoch, sourceVersion, recordDigest) {
|
|
412
|
-
const parts = ['candidate-
|
|
412
|
+
const parts = ['candidate-v1', retrievalId, memoryId, sourceEpoch, sourceVersion, recordDigest]
|
|
413
413
|
return CANDIDATE_PREFIX + first32(sha256Str(JSON.stringify(parts)))
|
|
414
414
|
}
|
|
415
415
|
|