@a9i5k4/dsh-auto-memory 2.5.3 → 3.0.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 +189 -7
- package/README.zh-CN.md +189 -7
- package/docs/CONTRIBUTORS.html +471 -0
- package/docs/FRONTEND-CO-CREATION.md +191 -0
- package/docs/GM53-HOMEPAGE-PROMPT.md +323 -0
- package/docs/HANDOFF-CRITERIA.md +92 -0
- package/docs/HOMEPAGE-CONTENT-FOR-GM53.md +299 -0
- package/docs/INTEGRATION-ANALYSIS.md +350 -348
- package/docs/PROMO-PROMPT-3.0.md +100 -0
- package/docs/USER-GUIDE.en.md +58 -3
- package/docs/USER-GUIDE.zh-CN.md +59 -4
- package/docs/WHITEPAPER.md +207 -0
- 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/ARCHITECTURE-FOR-ZCODE-20260920.md +397 -0
- package/docs/internal/ART-DIRECTION-DEEPSEEK-20260920.md +351 -0
- package/docs/internal/ART-DIRECTION-WIREFRAME.md +191 -181
- package/docs/internal/ART-DIRECTION-WIREFRAME.md.bak-superseded +181 -0
- package/docs/internal/AUDIT-WB-GRAPH-FULL-20260916.md +314 -0
- package/docs/internal/BATTLE-PLAN-20260917.md +871 -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/FEATURE-INVENTORY.md +531 -0
- package/docs/internal/FEEDBACK-TO-DSHAPI-RELAY.md +13 -0
- package/docs/internal/G-SERIES-EXECUTION-20260917.md +248 -0
- package/docs/internal/G3-DESIGN-20260918.md +82 -0
- package/docs/internal/G3-DISK-FORMAT-GAP-20260919.md +92 -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/HANDOFF-TO-ZCODE-20260920.md +309 -0
- package/docs/internal/HERMES-DATA-VERIFICATION-20260919.md +120 -0
- package/docs/internal/HERMES-LEGACY-STATUS-20260919.md +74 -0
- package/docs/internal/ISSUE-55-58-VERIFICATION-20260918.md +175 -0
- package/docs/internal/ISSUE10-FIX-EXECUTION-20260919.md +389 -0
- package/docs/internal/ISSUE10-PLAN-20260919.md +254 -0
- package/docs/internal/ISSUE10B-FORENSICS-20260919.md +468 -0
- package/docs/internal/ISSUE9-PURGE-AND-R1-PLAIN-20260919.md +150 -0
- package/docs/internal/ISSUE9-RESIDUAL-FORENSICS-20260919.md +114 -0
- package/docs/internal/KICKOFF-P0.md +254 -0
- package/docs/internal/LESSON-TO-CANDIDATE-STATUS-20260919.md +79 -0
- package/docs/internal/MASTER-PLAN-3.0.md +411 -0
- package/docs/internal/MEMORY-GOVERNANCE-20260917.md +309 -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/PRE-FRONTEND-CHECKLIST-20260919.md +705 -0
- package/docs/internal/PRE-FRONTEND-CHECKLIST-20260919.md.bak-s10 +649 -0
- package/docs/internal/PROCEDURAL-MEMORY-AND-APPROVAL-DESIGN-20260918.md +225 -0
- package/docs/internal/PROGRESS-20260917.md +93 -0
- package/docs/internal/PROMPT-GAP-AUDIT-20260920.md +128 -0
- package/docs/internal/R1-DEGRADE-AUDIT-20260918.md +163 -0
- package/docs/internal/R1-READABILITY-FORENSICS-20260919.md +127 -0
- package/docs/internal/R2-EVIDENCE-DEEP-AUDIT-20260918.md +140 -0
- package/docs/internal/R3-DEGRADE-LEDGER-DESIGN-20260918.md +138 -0
- package/docs/internal/R4-RECALL-QUOTA-PLAN-20260918.md +218 -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/RESUME-20260918.md +171 -0
- package/docs/internal/RESUME-20260919.md +104 -0
- package/docs/internal/REVIEW-WB-GRAPH-SELF.md +81 -0
- package/docs/internal/RHINELAB-TO-DEEPSEEK-FEASIBILITY.md +198 -0
- package/docs/internal/ROADMAP-20260917-WEEK.md +439 -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 +185 -0
- package/docs/internal/S10-GAP-INVENTORY-20260917.md +239 -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/T6-EXECUTION-20260920.md +130 -0
- package/docs/internal/TELEMETRY-EFFECT-REPORT-DESIGN-20260918.md +146 -0
- package/docs/internal/THESIS-GAP-ANALYSIS-20260918.md +89 -0
- package/docs/internal/THESIS-OUTLINE-20260918.md +147 -0
- package/docs/internal/THREE-LAYER-CONTRACT.md +219 -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/UPSTREAM-ISSUE-PR-TRIAGE-20260919.md +297 -0
- package/docs/internal/UPSTREAM-ISSUES-3RD-AUDIT-20260920.md +104 -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/docs/screenshots/promo/promo-0-banner-v3.png +0 -0
- package/lib/acceptance.js +71 -0
- package/lib/activation-host.js +153 -18
- package/lib/activation-inbox.js +25 -7
- package/lib/board-mode.js +30 -0
- package/lib/client.js +1758 -90
- package/lib/config-io.js +156 -0
- package/lib/context-bridge.js +5 -2
- package/lib/context-host.js +86 -15
- package/lib/degrade.js +385 -0
- package/lib/dsh-home.js +143 -0
- package/lib/engine-identity.js +149 -0
- package/lib/engine-switch.js +247 -0
- package/lib/episodic-store.js +63 -12
- package/lib/evidence-store.js +10 -3
- package/lib/fact-store.js +22 -3
- package/lib/fs-retry.js +46 -0
- package/lib/index-sync.js +13 -1
- package/lib/index.js +3446 -263
- package/lib/intent-clean-safe.js +258 -0
- package/lib/intent-clean.js +12 -16
- package/lib/l0-extract.js +478 -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/m4-corpus.js +8 -2
- package/lib/m7-index-sync-host.js +73 -5
- package/lib/m7-wire.js +3 -3
- package/lib/memory-anchor.js +56 -1
- package/lib/memory-envelope.js +257 -0
- package/lib/memory-hub.js +138 -13
- package/lib/memory-index.js +4 -2
- package/lib/memory-mutation.js +246 -0
- package/lib/memory-writer.js +204 -24
- package/lib/note-status-apply.js +118 -0
- package/lib/note-status.js +196 -0
- package/lib/procedure-observation.js +48 -0
- package/lib/procedure-store.js +118 -20
- package/lib/python-setup.js +1 -1
- package/lib/python-sidecar-client.js +29 -3
- package/lib/recall-fusion.js +83 -12
- package/lib/rerank-host.js +160 -0
- package/lib/rules-edit.js +159 -0
- package/lib/rules-layer.js +261 -0
- package/lib/semantic-decide.js +41 -8
- package/lib/semantic-js.js +66 -6
- package/lib/shadow-host.js +3 -5
- package/lib/shadow-retrieval.js +3 -3
- package/lib/skill-export-host.js +153 -0
- package/lib/skill-export.js +239 -0
- package/lib/state-commit.js +245 -0
- package/lib/storage-manage.js +6 -0
- package/lib/subagent-gc.js +4 -8
- package/lib/temporal-parse.js +191 -159
- package/lib/tier-layer-inject.js +650 -0
- package/lib/tier0-catalog.js +735 -0
- package/lib/water-window.js +263 -186
- package/lib/wb-contract.js +691 -0
- package/lib/wb-sidecar.js +890 -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,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 结论层状态(G3)· **纯函数核心** —— 零 IO、零副作用、可单测。
|
|
3
|
+
*
|
|
4
|
+
* ⚠️ **状态:形态候选,未接线**(2026-09-19)
|
|
5
|
+
* 本模块只提供**判定与格式**能力;**没有任何调用方**,因此不产生任何写入。
|
|
6
|
+
* 写盘接线必须等 `G3-DISK-FORMAT-GAP-20260919.md` §4 的 **F1/F2 拍板**后才做:
|
|
7
|
+
* F1 = 状态行的磁盘语法;F2 = 「显式声明取代」的模型侧写法。
|
|
8
|
+
* 理由(误判代价不对称):漏判 = 维持现状(可接受);**误判 = 有效结论被当废纸**。
|
|
9
|
+
*
|
|
10
|
+
* ── 为什么需要本模块(设计稿未覆盖的缺口)─────────────────────────
|
|
11
|
+
* `MEMORY-GOVERNANCE-20260917.md` §8.1 断言「地基已存在,只差写入方」。
|
|
12
|
+
* 实施前核查证明:该断言**只对内存索引列成立**;`status` 的**磁盘表示完全不存在**:
|
|
13
|
+
* ① `memory-anchor.js:28` `MARKER_RE` 是**严格锚定** ⇒ 锚点行加不了属性;
|
|
14
|
+
* ② 行首含 `MARKER_OPEN` 但不匹配 ⇒ `malformed-anchor` **冲突**(:214)⇒ 写入被拒(storage-manage:181);
|
|
15
|
+
* ③ 全仓**零处**从磁盘读 `status`(`statusOf` 无任何生产者);
|
|
16
|
+
* ④ sidecar 不能承载(`memory-writer.js:434` 明示其为**可重建的派生数据**)。
|
|
17
|
+
* ⇒ 必须先定一个**磁盘形态**,这正是 F1 要拍的板。
|
|
18
|
+
*
|
|
19
|
+
* ── 形态选择(方案 A/B 已排除,见 GAP 文档 §3)────────────────────
|
|
20
|
+
* 方案 A(锚点加属性)❌ 撞 ①②,属契约级改动;
|
|
21
|
+
* 方案 B(独立状态文件)❌ 违反 S10.4「不新建状态源」;
|
|
22
|
+
* **方案 C(条目正文内保留行)✅ 采用** —— 状态归条目自身,零语法风险,用户可读。
|
|
23
|
+
*
|
|
24
|
+
* ── ★ 关键实现约束:不得污染 L0 ──────────────────────────────────
|
|
25
|
+
* `extractL0Pre` 规则③(`l0-extract.js:385`)会把**所有非标题行压平**成 L0。
|
|
26
|
+
* 若状态行留在正文里,无标题条目的 L0 会变成「⚠已作废…」⇒ **检索质量被污染**
|
|
27
|
+
* (与 M2.5a 修的是同一类问题)。
|
|
28
|
+
* ⇒ 消费方**必须先 `stripStatusLinePre(body)` 再抽取 L0**,或经 `statusOf` 提供状态。
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/** 状态行开标记。**刻意不复用** `MARKER_OPEN`(`<!-- memory:`)—— 复用会撞锚点契约。 */
|
|
32
|
+
export const NOTE_STATUS_OPEN_V1 = '<!-- dsh-status:'
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 状态行语法(F1 候选 (a):HTML 注释形态)。
|
|
36
|
+
*
|
|
37
|
+
* 为什么用 HTML 注释而非裸行 `status: superseded`:
|
|
38
|
+
* ① 与仓储既有习俗一致(锚点本身也是 HTML 注释);
|
|
39
|
+
* ② 与正文散文**零碰撞**(裸行会被「正文恰好以 status: 开头」误命中);
|
|
40
|
+
* ③ 渲染后不可见,不干扰人读 Markdown。
|
|
41
|
+
* 属性值支持裸词与双引号两种写法(reason 含空格时用引号)。
|
|
42
|
+
*/
|
|
43
|
+
const NOTE_STATUS_RE = /^<!--\s*dsh-status:\s*(current|superseded|retracted)\s*((?:\w+=(?:"[^"]*"|[^\s"]+)\s*)*)-->$/
|
|
44
|
+
|
|
45
|
+
/** 属性解析:`by=mem_xxx` / `reason="含空格 的原因"`。 */
|
|
46
|
+
const NOTE_STATUS_ATTR_RE = /(\w+)=(?:"([^"]*)"|([^\s"]+))/g
|
|
47
|
+
|
|
48
|
+
/** 只认本仓 id 形态,防把任意文本拼进状态(同 R4-A 的 `supersededMarkPre` 纪律)。 */
|
|
49
|
+
const MEMORY_ID_RE = /^mem_[0-9a-f]{32}$/
|
|
50
|
+
|
|
51
|
+
/** 三态取值域(与 `l0-extract.js` 的 `L0_STATUSES` 同源;此处独立声明以免循环依赖)。 */
|
|
52
|
+
export const NOTE_STATUSES_V1 = Object.freeze(['current', 'superseded', 'retracted'])
|
|
53
|
+
|
|
54
|
+
/** reason 上限:状态行是**元数据**不是正文,超长即视为脏数据。 */
|
|
55
|
+
export const NOTE_STATUS_REASON_MAX_V1 = 120
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 渲染状态行(**纯函数**)。
|
|
59
|
+
*
|
|
60
|
+
* `current` ⇒ 返回 `''`(**当前态无需落盘**:缺失即默认 current,少写字节、少一处漂移源)。
|
|
61
|
+
* 未知 status / 非法 id ⇒ 返回 `''`(**fail-closed**:宁可不写,绝不写错 —— 与消费侧同纪律)。
|
|
62
|
+
*
|
|
63
|
+
* @param {string} status current | superseded | retracted
|
|
64
|
+
* @param {{supersededBy?:string, reason?:string}} [opts]
|
|
65
|
+
* @returns {string} 状态行(含 `\n` 时不带);不可渲染时为空串
|
|
66
|
+
*/
|
|
67
|
+
export function renderStatusLinePre(status, opts = {}) {
|
|
68
|
+
try {
|
|
69
|
+
if (status !== 'superseded' && status !== 'retracted') return ''
|
|
70
|
+
let line = NOTE_STATUS_OPEN_V1 + ' ' + status
|
|
71
|
+
const by = opts && opts.supersededBy ? String(opts.supersededBy).trim() : ''
|
|
72
|
+
if (MEMORY_ID_RE.test(by)) line += ' by=' + by
|
|
73
|
+
const reason = opts && opts.reason
|
|
74
|
+
? String(opts.reason).replace(/[\r\n]+/g, ' ').replace(/--+>/g, '').trim().slice(0, NOTE_STATUS_REASON_MAX_V1)
|
|
75
|
+
: ''
|
|
76
|
+
if (reason) line += ' reason="' + reason.replace(/"/g, "'") + '"'
|
|
77
|
+
return line + ' -->'
|
|
78
|
+
} catch (_) { return '' } // fail-soft:渲染失败绝不抛(与 supersededMarkPre 同纪律)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 解析单行状态。非状态行 ⇒ `null`(不抛、不猜)。
|
|
83
|
+
*
|
|
84
|
+
* 返回对象**只含合法字段**:非法 `by` 被丢弃(不写进结果),未知属性忽略。
|
|
85
|
+
*
|
|
86
|
+
* @param {string} line 单行文本
|
|
87
|
+
* @returns {{status:string, supersededBy?:string, reason?:string}|null}
|
|
88
|
+
*/
|
|
89
|
+
export function parseStatusLinePre(line) {
|
|
90
|
+
try {
|
|
91
|
+
const s = String(line == null ? '' : line).trim()
|
|
92
|
+
if (!s) return null
|
|
93
|
+
const m = NOTE_STATUS_RE.exec(s)
|
|
94
|
+
if (!m) return null
|
|
95
|
+
const out = { status: m[1] }
|
|
96
|
+
NOTE_STATUS_ATTR_RE.lastIndex = 0
|
|
97
|
+
let a
|
|
98
|
+
while ((a = NOTE_STATUS_ATTR_RE.exec(m[2] || '')) !== null) {
|
|
99
|
+
const key = a[1]
|
|
100
|
+
const val = a[2] !== undefined ? a[2] : a[3]
|
|
101
|
+
if (key === 'by') {
|
|
102
|
+
if (MEMORY_ID_RE.test(String(val || ''))) out.supersededBy = String(val)
|
|
103
|
+
} else if (key === 'reason') {
|
|
104
|
+
const r = String(val || '').replace(/[\r\n]+/g, ' ').trim().slice(0, NOTE_STATUS_REASON_MAX_V1)
|
|
105
|
+
if (r) out.reason = r
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out
|
|
109
|
+
} catch (_) { return null }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 从**条目正文**解析状态。
|
|
114
|
+
*
|
|
115
|
+
* 语义:**最后一个**状态行胜出(后写覆盖先写 —— 状态是"当前值"不是"历史轨迹")。
|
|
116
|
+
* 无状态行 ⇒ `{status:'current'}`(与索引层「缺失即默认」口径完全一致)。
|
|
117
|
+
*
|
|
118
|
+
* @param {string} body 条目正文(不含锚点行)
|
|
119
|
+
* @returns {{status:string, supersededBy?:string, reason?:string}}
|
|
120
|
+
*/
|
|
121
|
+
export function statusOfBodyPre(body) {
|
|
122
|
+
const s = String(body == null ? '' : body)
|
|
123
|
+
const lines = s.split(/\r?\n/)
|
|
124
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
125
|
+
const hit = parseStatusLinePre(lines[i])
|
|
126
|
+
if (hit) return hit
|
|
127
|
+
}
|
|
128
|
+
return { status: 'current' }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 剥掉状态行,返回**可安全送往 L0 抽取**的正文。
|
|
133
|
+
*
|
|
134
|
+
* ★ 这是本模块存在的主要理由之一:不剥 ⇒ 无标题条目的 L0 会被状态行污染(见文件头注释)。
|
|
135
|
+
* 纪律:只删**整行**匹配的;行内出现(如正文引用该语法)**保持原样**交由
|
|
136
|
+
* `checkReservedSyntaxInContent` 一类守卫处理,本函数不做静默改写。
|
|
137
|
+
*
|
|
138
|
+
* @param {string} body
|
|
139
|
+
* @returns {string}
|
|
140
|
+
*/
|
|
141
|
+
export function stripStatusLinePre(body) {
|
|
142
|
+
const s = String(body == null ? '' : body)
|
|
143
|
+
if (!s.includes(NOTE_STATUS_OPEN_V1)) return s
|
|
144
|
+
return s.split(/\r?\n/).filter((ln) => parseStatusLinePre(ln) === null).join('\n')
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 追加状态行到正文末尾(**纯函数**,不写盘)。
|
|
149
|
+
*
|
|
150
|
+
* 约束(见文件头):
|
|
151
|
+
* - 先剥旧状态行 ⇒ 结果是**幂等**的(同参数重复调用不叠加);
|
|
152
|
+
* - 只追加在**末尾**,绝不前置(前置会污染 L0 规则②/③);
|
|
153
|
+
* - `current` ⇒ 只剥不写。
|
|
154
|
+
*
|
|
155
|
+
* @param {string} body
|
|
156
|
+
* @param {string} status
|
|
157
|
+
* @param {{supersededBy?:string, reason?:string}} [opts]
|
|
158
|
+
* @returns {string}
|
|
159
|
+
*/
|
|
160
|
+
export function withStatusLinePre(body, status, opts = {}) {
|
|
161
|
+
const base = stripStatusLinePre(body).replace(/[\r\n\s]+$/, '')
|
|
162
|
+
const line = renderStatusLinePre(status, opts)
|
|
163
|
+
if (!line) return base
|
|
164
|
+
return base + '\n' + line
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* ★F2 候选 (b):从**正文**识别「显式声明取代」。
|
|
169
|
+
*
|
|
170
|
+
* ⚠️ 本函数**未接线**,且**不建议作为首个版本的唯一通路**:
|
|
171
|
+
* 用户裁定的门槛是「**(a) 只认显式声明**」,而**结构化参数**(F2 候选 a)才是最纯的显式声明;
|
|
172
|
+
* 从自然语言里正则识别**必然**存在误判,与「宁可漏判不可误判」冲突。
|
|
173
|
+
* 保留它用于:(b)/(c) 落地时作为**第二通路**,或作为「M2 lint 只报告」的输入。
|
|
174
|
+
*
|
|
175
|
+
* 判据(**双条件**,缺一不可 —— 这是保守性的关键):
|
|
176
|
+
* ① 出现取代**动作词**(取代/替换/作废/推翻/废弃 supersede)/ 或显式参数;
|
|
177
|
+
* ② 同时出现**合法 memoryId**。
|
|
178
|
+
* 仅命中其一 ⇒ 返回 `null`(**只报告不标**)。
|
|
179
|
+
*
|
|
180
|
+
* @param {string} content 新写入的正文
|
|
181
|
+
* @returns {{target:string, evidence:string}|null}
|
|
182
|
+
*/
|
|
183
|
+
export function detectSupersedeIntentPre(content) {
|
|
184
|
+
try {
|
|
185
|
+
const s = String(content == null ? '' : content)
|
|
186
|
+
if (!s) return null
|
|
187
|
+
const ACT_RE = /(取代|替换|作废|废弃|推翻|supersede[sd]?|replaces?)/i
|
|
188
|
+
const ID_RE = /mem_[0-9a-f]{32}/g
|
|
189
|
+
const ids = s.match(ID_RE)
|
|
190
|
+
if (!ids || !ids.length) return null
|
|
191
|
+
if (!ACT_RE.test(s)) return null
|
|
192
|
+
// 去重保序;多目标时**不自动全标**(保守),交调用方逐个确认
|
|
193
|
+
const uniq = [...new Set(ids)]
|
|
194
|
+
return { target: uniq[0], evidence: (s.match(ACT_RE) || [''])[0] }
|
|
195
|
+
} catch (_) { return null }
|
|
196
|
+
}
|
|
@@ -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
|
}
|
|
@@ -250,20 +261,46 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
250
261
|
/**
|
|
251
262
|
* 晋升判定(M-04 元代码 promote 逐行)。
|
|
252
263
|
* 返回 { decision: 'promote'|'keep'|'ask', procedure, reasonCodes }
|
|
264
|
+
*
|
|
265
|
+
* ★ T4(2026-09-19 用户拍板「让大模型来介入」):新增第三个参数 `opts`。
|
|
266
|
+
* `opts.authorizedBy`(如 'model')= **模型显式授权**,用于跳过两条**统计证据门**
|
|
267
|
+
* (`minSessionDiversity` / `minSuccessCount`)。
|
|
268
|
+
*
|
|
269
|
+
* 为什么需要它:这两条门是给**机械生成**的观察行用的防污染护栏 —— 它们靠"反复出现"累积证据。
|
|
270
|
+
* 但机械路径产出的条目 13/14 是空壳(`actions=['user','user','user']` 切出来的),
|
|
271
|
+
* 而**模型主动写出的技能**(带真 steps + successCriteria)**天生没有历史证据**,
|
|
272
|
+
* 若不放行则永远卡在 `diversity-below-3` ⇒ 模型通路等于白建。
|
|
273
|
+
*
|
|
274
|
+
* **仍然保留的护栏(授权也不放行)**:
|
|
275
|
+
* · `deprecated` 短路 —— 已弃用的不得复活;
|
|
276
|
+
* · `isObservationOnlyPre` 短路 —— 结构上不该晋升的行,授权也拦;
|
|
277
|
+
* · **`no-success-criteria` 必须有**(在下方)—— 没写验收标准的不算技能;
|
|
278
|
+
* · `correction-rate` / `has-correction` —— 有纠正记录说明这流程是错的。
|
|
279
|
+
* 并**留痕**:`p.authorizedBy` + reasonCodes 追加 `model-authorized`,供审计与前端展示。
|
|
253
280
|
*/
|
|
254
|
-
function promote(procedureId, extraEvidence = {}) {
|
|
281
|
+
function promote(procedureId, extraEvidence = {}, opts = {}) {
|
|
255
282
|
if (disposed) return { ok: false, reason: 'disposed' }
|
|
256
283
|
const p = procedures.find((x) => x.procedureId === procedureId)
|
|
257
284
|
if (!p) return { ok: false, reason: 'not-found' }
|
|
258
285
|
if (p.stage === 'deprecated') return { ok: true, decision: 'keep', reasonCodes: ['deprecated'] }
|
|
286
|
+
// issue #30:纯 episode 观察行没有 successCriteria,本来就无法晋升;
|
|
287
|
+
// 显式短路并给出 `observation-only` 原因码,而不是让它落到下面报 `no-success-criteria`
|
|
288
|
+
// (后者会让使用者误以为"缺判据、补上就能晋升",其实是这行**结构上**不该晋升)。
|
|
289
|
+
// ★ T4:此短路**不受授权影响** —— 结构上不该晋升的行,模型授权也不放行。
|
|
290
|
+
if (isObservationOnlyPre(p)) return { ok: true, decision: 'keep', procedure: p, reasonCodes: ['observation-only'] }
|
|
259
291
|
|
|
260
292
|
const ev = p.evidence
|
|
261
293
|
const reason = []
|
|
294
|
+
const authorizedBy = opts && opts.authorizedBy ? String(opts.authorizedBy) : ''
|
|
295
|
+
if (authorizedBy) reason.push('model-authorized')
|
|
262
296
|
// 跨会话多样性
|
|
263
297
|
const diversity = extraEvidence.distinctSessions != null ? extraEvidence.distinctSessions : ev.sessions
|
|
264
298
|
const successCount = extraEvidence.successCount != null ? extraEvidence.successCount : ev.success
|
|
265
|
-
|
|
266
|
-
if (
|
|
299
|
+
// ★ T4:两条**统计证据门**在模型授权下跳过(见上方函数注释);未授权时行为逐字节不变。
|
|
300
|
+
if (!authorizedBy) {
|
|
301
|
+
if (diversity < gates.minSessionDiversity) { reason.push('diversity-below-' + gates.minSessionDiversity); return { ok: true, decision: 'keep', procedure: p, reasonCodes: reason } }
|
|
302
|
+
if (successCount < gates.minSuccessCount) { reason.push('success-below-' + gates.minSuccessCount); return { ok: true, decision: 'keep', procedure: p, reasonCodes: reason } }
|
|
303
|
+
}
|
|
267
304
|
// correction 率
|
|
268
305
|
const total = ev.seen + ev.read + ev.cite + ev.reuse + ev.success + ev.correction
|
|
269
306
|
const corrRate = total > 0 ? ev.correction / total : 0
|
|
@@ -287,11 +324,68 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
287
324
|
// 晋升
|
|
288
325
|
p.stage = 'validated'
|
|
289
326
|
p.updatedAt = nowFn()
|
|
327
|
+
// ★ T4:授权晋升留痕 —— 前端与审计面必须能看出「这条是模型授权跳门进来的」,
|
|
328
|
+
// 否则日后无法区分「统计证据充分」与「模型判断值得」两种来源。
|
|
329
|
+
if (authorizedBy) p.authorizedBy = authorizedBy
|
|
290
330
|
stats.validated++
|
|
291
331
|
void persist()
|
|
292
332
|
return { ok: true, decision: 'promote', procedure: p, reasonCodes: reason }
|
|
293
333
|
}
|
|
294
334
|
|
|
335
|
+
/**
|
|
336
|
+
* ★R2(2026-09-20 用户要求「晋升原因必须看得见」):**纯只读**晋升判定投影。
|
|
337
|
+
*
|
|
338
|
+
* 与 `promote()` 的关系:**逐行复制其门限判定,但不改任何状态**。
|
|
339
|
+
* 为什么不能直接调 `promote()`:它含真实副作用
|
|
340
|
+
* (`stats.approvalAsked++` / `p.stage='validated'` / `stats.validated++` / `persist()` 写盘),
|
|
341
|
+
* 在 `overview()` 这类只读轮询路径里调用会**污染统计并把候选悄悄提升**。
|
|
342
|
+
* ⇒ 只读面必须走本函数(用户 2026-09-18 冻结纪律)。
|
|
343
|
+
*
|
|
344
|
+
* ⚠️ 维护约定:改 `promote()` 的门限时**必须同步改这里**,否则前端会展示过时的判据。
|
|
345
|
+
* 两者的一致性由测试 `smoke-test-r2-promotion-pre.mjs` 用变异兜底。
|
|
346
|
+
*
|
|
347
|
+
* @returns {{ok:boolean, decision?:string, reasonCodes?:string[], detail?:object}}
|
|
348
|
+
*/
|
|
349
|
+
function evaluatePromotion(procedureId) {
|
|
350
|
+
const p = procedures.find((x) => x.procedureId === procedureId)
|
|
351
|
+
if (!p) return { ok: false, reason: 'not-found' }
|
|
352
|
+
if (p.stage === 'deprecated') return { ok: true, decision: 'keep', reasonCodes: ['deprecated'] }
|
|
353
|
+
if (isObservationOnlyPre(p)) return { ok: true, decision: 'keep', reasonCodes: ['observation-only'] }
|
|
354
|
+
const ev = p.evidence || { seen: 0, read: 0, cite: 0, reuse: 0, success: 0, correction: 0, sessions: 0 }
|
|
355
|
+
const reason = []
|
|
356
|
+
const diversity = Number(ev.sessions) || 0
|
|
357
|
+
const successCount = Number(ev.success) || 0
|
|
358
|
+
const total = (Number(ev.seen) || 0) + (Number(ev.read) || 0) + (Number(ev.cite) || 0)
|
|
359
|
+
+ (Number(ev.reuse) || 0) + successCount + (Number(ev.correction) || 0)
|
|
360
|
+
const corrRate = total > 0 ? (Number(ev.correction) || 0) / total : 0
|
|
361
|
+
// ⚠️ 顺序必须与 promote() **完全一致**(先返回的那个才决定前端文案)
|
|
362
|
+
if (diversity < gates.minSessionDiversity) {
|
|
363
|
+
reason.push('diversity-below-' + gates.minSessionDiversity)
|
|
364
|
+
return { ok: true, decision: 'keep', reasonCodes: reason, detail: { diversity, need: gates.minSessionDiversity } }
|
|
365
|
+
}
|
|
366
|
+
if (successCount < gates.minSuccessCount) {
|
|
367
|
+
reason.push('success-below-' + gates.minSuccessCount)
|
|
368
|
+
return { ok: true, decision: 'keep', reasonCodes: reason, detail: { successCount, need: gates.minSuccessCount } }
|
|
369
|
+
}
|
|
370
|
+
if (corrRate > gates.maxCorrectionRate) {
|
|
371
|
+
reason.push('correction-rate-' + corrRate.toFixed(2))
|
|
372
|
+
return { ok: true, decision: 'keep', reasonCodes: reason, detail: { corrRate, cap: gates.maxCorrectionRate } }
|
|
373
|
+
}
|
|
374
|
+
if (gates.maxContradictions === 0 && (Number(ev.correction) || 0) > 0) {
|
|
375
|
+
reason.push('has-correction')
|
|
376
|
+
return { ok: true, decision: 'keep', reasonCodes: reason, detail: { corrections: Number(ev.correction) || 0 } }
|
|
377
|
+
}
|
|
378
|
+
if (!p.successCriteria || !p.successCriteria.length) {
|
|
379
|
+
reason.push('no-success-criteria')
|
|
380
|
+
return { ok: true, decision: 'keep', reasonCodes: reason }
|
|
381
|
+
}
|
|
382
|
+
if (p.requiresApproval && !p.approved) {
|
|
383
|
+
reason.push('high-risk-awaiting-approval')
|
|
384
|
+
return { ok: true, decision: 'ask', reasonCodes: reason }
|
|
385
|
+
}
|
|
386
|
+
return { ok: true, decision: 'promote', reasonCodes: reason, detail: { diversity, successCount } }
|
|
387
|
+
}
|
|
388
|
+
|
|
295
389
|
/** 激活: validated → active(可被召回)。 */
|
|
296
390
|
function activate(procedureId) {
|
|
297
391
|
const p = procedures.find((x) => x.procedureId === procedureId)
|
|
@@ -377,7 +471,9 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
377
471
|
const v = validateProcedurePre(p)
|
|
378
472
|
if (!v.ok) continue
|
|
379
473
|
if (p._sessions) { p._sessions = new Set(p._sessions); p.evidence.sessions = p._sessions.size }
|
|
380
|
-
|
|
474
|
+
// issue #30:旧快照里没有 observationOnly 字段,恢复时按同一判据补齐
|
|
475
|
+
// (纯增量元数据迁移:不改身份/文本/风险/证据/阶段)。
|
|
476
|
+
procedures.push(normalizeProcedureObservationPre(v.procedure))
|
|
381
477
|
}
|
|
382
478
|
return { ok: true, restored: procedures.length }
|
|
383
479
|
}
|
|
@@ -396,6 +492,8 @@ export function createProcedureStorePre(opts = {}) {
|
|
|
396
492
|
}
|
|
397
493
|
|
|
398
494
|
return {
|
|
495
|
+
// ★R2:只读判定投影(供 overview() 展示「为什么不能晋升」)
|
|
496
|
+
evaluatePromotion,
|
|
399
497
|
observe, addEvidence, promote, activate, deprecate,
|
|
400
498
|
touch, setPinned, applyAutomaticTransitions,
|
|
401
499
|
query, activeProcedures, get, renderChecklist,
|
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
|
|
@@ -80,9 +80,14 @@ export function createPythonSidecarClientPre(opts = {}) {
|
|
|
80
80
|
if (disposed) return { ok: false, code: 'disposed' }
|
|
81
81
|
if (child && (child.killed || (child.stdin && child.stdin.destroyed))) {
|
|
82
82
|
// 上一个进程正在收尾(exit 事件未到):按已死处理,允许立即重生
|
|
83
|
+
// ★ issue #72 修复(2026-09-19):此处必须**同时清空共享 buffer**。
|
|
84
|
+
// 旧实现只置 `epoch = null`(等 `:121` 的 exit 事件去清 buffer),但这条分支的前提正是
|
|
85
|
+
// **exit 尚未到达** ⇒ 旧 worker 残留在 `buffer` 里的**无换行尾字节**会被新 worker 的首帧
|
|
86
|
+
// `concat` 进去 ⇒ 该行 badJson ⇒ 应答被吞、请求白等到超时(默认 5000ms)。
|
|
83
87
|
try { child.kill() } catch (_) {}
|
|
84
88
|
child = null
|
|
85
89
|
epoch = null
|
|
90
|
+
buffer = Buffer.alloc(0) // ★ 丢弃旧代残尾,避免污染新 worker 首帧
|
|
86
91
|
}
|
|
87
92
|
if (child) return { ok: true }
|
|
88
93
|
const scriptPath = String(resolveOpt('scriptPath') || defaultWorkerScriptPathPre())
|
|
@@ -161,7 +166,16 @@ export function createPythonSidecarClientPre(opts = {}) {
|
|
|
161
166
|
const v = validateTransportFramePre(obj, { direction: 'in' })
|
|
162
167
|
if (!v.ok) { stats.dropped.badEnvelope++; return }
|
|
163
168
|
const frame = v.frame
|
|
164
|
-
|
|
169
|
+
// ★ issue #72 修复(2026-09-19):把入站 epoch 门从 **fail-open 改为 fail-closed**,与文件头 :7 的承诺一致。
|
|
170
|
+
// 旧写法 `if (epoch !== null && frame.workerEpoch !== epoch)`:**`epoch === null` 时整条门失效**,
|
|
171
|
+
// 任何合法 envelope 的帧都会穿透到 handler。epoch 为 null 的窗口是真实存在的
|
|
172
|
+
// (未启动 / `exit` 事件已置 null 但旧 stdio 尚未排空 / restart 后到新 spawn 之间),
|
|
173
|
+
// 而 `activation_request` 上游 `context-host` **无二次校验**(同 activationId 有 dedup 兜底,
|
|
174
|
+
// 但**新 activationId 可穿透**)⇒ 幽灵激活。
|
|
175
|
+
// 新判据:**epoch 为空 ⇒ 一律丢弃**。理由:spawn 时同步赋 epoch(:91 `epoch = 'wk_' + …` 在
|
|
176
|
+
// `spawn()` **之前**执行),出站帧也就带上了 epoch;对端 worker 以 `--expect-epoch` 自证身份后才回帧
|
|
177
|
+
// ⇒ **正常路径不存在"帧先于 epoch"**,丢弃不会误伤任何合法流量。
|
|
178
|
+
if (!epoch || frame.workerEpoch !== epoch) { stats.dropped.staleEpoch++; return }
|
|
165
179
|
if (frame.type === 'activation_request') {
|
|
166
180
|
const activation = frame.payload && frame.payload.activation
|
|
167
181
|
const aid = activation && activation.activationId
|
|
@@ -208,9 +222,18 @@ export function createPythonSidecarClientPre(opts = {}) {
|
|
|
208
222
|
lastSentFrame = frame
|
|
209
223
|
if (!child || !child.stdin || child.stdin.destroyed) return false
|
|
210
224
|
const line = Buffer.from(JSON.stringify(frame) + '\n', 'utf8')
|
|
225
|
+
// ★ issue #75 CC-9 修复(2026-09-19):**建帧时捕获当时的 child 引用**,flush 时校验身份。
|
|
226
|
+
// 旧实现:`writeChain.then(...)` 回调里解引用**当时的**模块级 `child` 变量。
|
|
227
|
+
// 若 writeChain 排队期间发生 respawn(`ensureStarted` 的收尾中重生 / `restart()`),
|
|
228
|
+
// 回调会读到**新** child,把**旧 epoch 的帧**写进新 worker 的 stdin ⇒ 对端回帧 epoch-mismatch
|
|
229
|
+
// ⇒ 被判 `staleEpoch` 丢弃 ⇒ 该请求挂满自身超时(默认 5000ms)。
|
|
230
|
+
// 现改为闭包捕获 `sentTo`,且 flush 时 `child !== sentTo` 即视为该帧已随旧进程作废。
|
|
231
|
+
const sentTo = child
|
|
232
|
+
const sentStdin = child.stdin
|
|
211
233
|
writeChain = writeChain.then(() => new Promise((done) => {
|
|
212
|
-
if (
|
|
213
|
-
|
|
234
|
+
if (child !== sentTo) { done(); return } // 已换代:本帧作废,不得写入新 worker
|
|
235
|
+
if (!sentStdin || sentStdin.destroyed) { done(); return }
|
|
236
|
+
sentStdin.write(line, () => done())
|
|
214
237
|
}))
|
|
215
238
|
writeChain = writeChain.catch(() => {})
|
|
216
239
|
return true
|
|
@@ -282,6 +305,9 @@ export function createPythonSidecarClientPre(opts = {}) {
|
|
|
282
305
|
if (child) { try { child.kill() } catch (_) {} }
|
|
283
306
|
child = null
|
|
284
307
|
epoch = null
|
|
308
|
+
// ★ issue #72 修复:restart 同样要清 buffer(kill 后 exit 事件可能滞后,
|
|
309
|
+
// 旧代残尾会污染新 worker 首帧 ⇒ 该行 badJson ⇒ 请求白等到超时)。
|
|
310
|
+
buffer = Buffer.alloc(0)
|
|
285
311
|
stats.restarts++
|
|
286
312
|
void reason
|
|
287
313
|
}
|