@a9i5k4/dsh-auto-memory 2.2.6 → 2.3.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 +20 -10
- package/README.zh-CN.md +22 -10
- package/docs/CONTINUITY-FLOW.md +222 -0
- package/docs/HANDBOOK.md +354 -0
- package/docs/INTEGRATION-ANALYSIS.md +348 -0
- package/docs/M-CM7-HANDOFF-LAYERED-RETRIEVAL.md +311 -0
- package/docs/M8-MEMORY-HUB.md +1 -1
- package/docs/PROMPT-PACK-LAYERED-RECALL.md +474 -0
- package/docs/PROMPT-SET-STRICT.md +389 -0
- package/docs/RELEASE-GO-NOGO.md +82 -0
- package/docs/ROADMAP.md +162 -0
- package/docs/STATUS-BOARD.md +147 -0
- package/docs/USER-GUIDE.en.md +382 -0
- package/docs/USER-GUIDE.zh-CN.md +382 -289
- package/docs/prompts/EXEC-ORDER.md +77 -0
- package/docs/prompts/FEEDING-SCRIPT.md +174 -0
- package/docs/prompts/FEEDING-SEQUENCE.md +61 -0
- package/docs/prompts/FIX-AGENT-M8-2b.md +119 -0
- package/docs/prompts/FIX-AGENT-P11.md +97 -0
- package/docs/prompts/FIX-AGENT-P12-FULL-REGRESSION.md +135 -0
- package/docs/prompts/FIX-AGENT-P12.md +113 -0
- package/docs/prompts/FIX-AGENT-P13-PYTHON-RANK.md +100 -0
- package/docs/prompts/FIX-AGENT-P8.md +120 -0
- package/docs/prompts/FIX-AGENT-P9.md +110 -0
- package/docs/prompts/FIX-AGENT-P9a.md +94 -0
- package/docs/prompts/FIX-AGENT-P9d.md +114 -0
- package/docs/prompts/FIX-AGENT-TEMPORAL-ARM.md +148 -0
- package/docs/prompts/LIVE-VERIFY-ZCODE.md +105 -0
- package/docs/prompts/M8-1-fact-metadata.md +45 -0
- package/docs/prompts/M8-2-ADJUDICATION.md +98 -0
- package/docs/prompts/M8-2-importance-wiring.md +42 -0
- package/docs/prompts/M8-2b-evidence-pipeline.md +52 -0
- package/docs/prompts/M8-3-enable-verify.md +49 -0
- package/docs/prompts/M8-R-REPORT.md +156 -0
- package/docs/prompts/M8-R-research.md +67 -0
- package/docs/prompts/P1-l0-index.md +30 -0
- package/docs/prompts/P10-importance-calibration.md +45 -0
- package/docs/prompts/P11-silent-catch-observability.md +43 -0
- package/docs/prompts/P2-semantic-recall.md +30 -0
- package/docs/prompts/P3-fusion.md +28 -0
- package/docs/prompts/P4-l0-response.md +28 -0
- package/docs/prompts/P5-handoff-anchor.md +28 -0
- package/docs/prompts/P6-ledger-weight.md +27 -0
- package/docs/prompts/P7-write-fix.md +26 -0
- package/docs/prompts/P8-rrf-wiring.md +47 -0
- package/docs/prompts/P9-REVIEW-DECISION.md +95 -0
- package/docs/prompts/P9-evidence-write-coverage.md +113 -0
- package/docs/prompts/README.md +105 -0
- package/docs/prompts/ZCODE-DROPIN.md +229 -0
- package/docs/prompts/_COMMON.md +88 -0
- package/lib/client.js +36 -2
- package/lib/context-host.js +77 -2
- package/lib/evidence-agg.js +81 -0
- package/lib/fact-store.js +32 -0
- package/lib/handoff-anchor.js +114 -0
- package/lib/index.js +402 -43
- package/lib/l0-extract.js +149 -0
- package/lib/l0-index.js +239 -0
- package/lib/m7-wire.js +4 -3
- package/lib/memory-importance.js +70 -0
- package/lib/python-setup.js +16 -4
- package/lib/recall-fusion.js +99 -0
- package/lib/shadow-retrieval.js +2 -2
- package/lib/storage-manage.js +17 -0
- package/lib/subagent-gc.js +8 -1
- package/lib/temporal-parse.js +159 -0
- package/package.json +1 -1
- package/python/worker_semantic_v1.py +28 -1
package/lib/storage-manage.js
CHANGED
|
@@ -62,6 +62,22 @@ export function createStorageManagerPre(opts = {}) {
|
|
|
62
62
|
const catalog = catalogFor(paths)
|
|
63
63
|
if (!catalog) return { ok: false, reason: 'no-paths' }
|
|
64
64
|
if (!io.sidecarDir) return { ok: false, reason: 'no-sidecar-dir' }
|
|
65
|
+
// 2026-09-09(no-doc-store 报障):锚定索引层(memoryAnchorEnabled → docStore)未就位时,
|
|
66
|
+
// 缺 sidecar 属预期状态而非可修复的 stale——避免「报 stale 可修、修复恒 no-doc-store」死循环。
|
|
67
|
+
// 顶层 indexEnabled=false 交 UI 提示;语料文件本身仍判 ok(健康),只是无索引可比对。
|
|
68
|
+
const ds = docStoreOf()
|
|
69
|
+
const indexEnabled = !!(ds && typeof ds.sidecarPath === 'function')
|
|
70
|
+
if (!indexEnabled) {
|
|
71
|
+
const sources = catalog.sources.map((s) => ({
|
|
72
|
+
sourceRef: s.sourceRef, kind: s.kind, scope: s.scope, file: s.file,
|
|
73
|
+
status: 'ok', reasons: [], repairable: [],
|
|
74
|
+
}))
|
|
75
|
+
return {
|
|
76
|
+
ok: true, indexEnabled: false, scannedAt: now(), version: STORAGE_MANAGE_VERSION_V1,
|
|
77
|
+
sources, stale: [],
|
|
78
|
+
counts: { total: sources.length, ok: sources.length, stale: 0, unrepairable: 0 },
|
|
79
|
+
}
|
|
80
|
+
}
|
|
65
81
|
const res = loadCorpusSnapshot(catalog, io)
|
|
66
82
|
const dropped = (res && res.dropped) || []
|
|
67
83
|
const byRef = new Map()
|
|
@@ -81,6 +97,7 @@ export function createStorageManagerPre(opts = {}) {
|
|
|
81
97
|
const stale = sources.filter((s) => s.status === 'stale')
|
|
82
98
|
const out = {
|
|
83
99
|
ok: true,
|
|
100
|
+
indexEnabled: true,
|
|
84
101
|
scannedAt: now(),
|
|
85
102
|
version: STORAGE_MANAGE_VERSION_V1,
|
|
86
103
|
sources,
|
package/lib/subagent-gc.js
CHANGED
|
@@ -220,7 +220,14 @@ export async function recycleSessions(opts = {}) {
|
|
|
220
220
|
if (await exists(destDir)) { out.skipped.push({ sid: c.sid, reason: 'backup-exists' }); continue }
|
|
221
221
|
if (!apply) { out.moved.push({ sid: c.sid, label: c.label, sizeBytes: c.sizeBytes, to: destDir, dryRun: true }); out.bytes += c.sizeBytes; continue }
|
|
222
222
|
await mkdir(path.dirname(destDir), { recursive: true })
|
|
223
|
-
|
|
223
|
+
// Windows 下 rename 对被占用目录抛 EPERM(宿主/杀软句柄未释放)→ 短退避重试,仍失败则跳过等下次
|
|
224
|
+
let renamed = false
|
|
225
|
+
let lastErr = null
|
|
226
|
+
for (const delay of [0, 250, 800, 2000]) {
|
|
227
|
+
if (delay) await new Promise((r) => setTimeout(r, delay))
|
|
228
|
+
try { await rename(c.dir, destDir); renamed = true; break } catch (e) { lastErr = e }
|
|
229
|
+
}
|
|
230
|
+
if (!renamed) throw lastErr
|
|
224
231
|
out.moved.push({ sid: c.sid, label: c.label, sizeBytes: c.sizeBytes, to: destDir })
|
|
225
232
|
out.bytes += c.sizeBytes
|
|
226
233
|
if (projcacheRoot) {
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* temporal-parse-pre —— 中文时间表达解析(P12 后新增时间检索臂 Phase 1, 2026-09-09)。
|
|
3
|
+
*
|
|
4
|
+
* 目标:把查询里的中文时间表达("上周""三天前""最近一周""上个月"等)解析为
|
|
5
|
+
* [startMs, endMs) 时间范围,供检索的软性第三臂(time arm)做 rank-space 提升。
|
|
6
|
+
* 查询无时间表达 → 返回 null → 调用方零行为变更。
|
|
7
|
+
*
|
|
8
|
+
* 边界:纯函数、零 IO、零依赖;禁止 LLM/第三方日期库;确定性(同输入同 now 逐字节相同输出)。
|
|
9
|
+
* 时刻必须经 opts.now 注入(缺省 Date.now() 仅为便利;测试一律注入)。
|
|
10
|
+
*
|
|
11
|
+
* 时区假设(显式声明):全部自然日/周/月/年边界用宿主**本地时间**计算
|
|
12
|
+
* (new Date(y,m,d) 系列本地方法)。部署机时区为 Asia/Shanghai(CST,UTC+8,无夏令时),
|
|
13
|
+
* 与记忆日志文件名日期(本地日期)一致;若部署到其他时区,范围语义跟随宿主本地时区。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** 中文数字表(支持零〇一二两三四五六七八九十百,≤999,够 366 上限)。 */
|
|
17
|
+
const CN_DIGIT = { '零': 0, '〇': 0, '一': 1, '二': 2, '两': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9 }
|
|
18
|
+
|
|
19
|
+
/** N 的合法区间(含端点);超界视为未识别。 */
|
|
20
|
+
const N_MIN = 1
|
|
21
|
+
const N_MAX = 366
|
|
22
|
+
|
|
23
|
+
const DAY_MS = 86400000
|
|
24
|
+
|
|
25
|
+
/** 中文/阿拉伯数字 → 整数;非法返回 NaN。 */
|
|
26
|
+
function numToInt(s) {
|
|
27
|
+
if (/^\d{1,3}$/.test(s)) return parseInt(s, 10)
|
|
28
|
+
let total = 0
|
|
29
|
+
let num = 0
|
|
30
|
+
for (const ch of String(s)) {
|
|
31
|
+
if (ch === '十') { total += (num || 1) * 10; num = 0 }
|
|
32
|
+
else if (ch === '百') { total += (num || 1) * 100; num = 0 }
|
|
33
|
+
else if (ch === '零' || ch === '〇') { /* 占位,跳过 */ }
|
|
34
|
+
else if (CN_DIGIT[ch] !== undefined) { num = CN_DIGIT[ch] }
|
|
35
|
+
else return NaN
|
|
36
|
+
}
|
|
37
|
+
const out = total + num
|
|
38
|
+
return out > 0 ? out : NaN
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 本地自然日 00:00 的毫秒时间戳。 */
|
|
42
|
+
function startOfDay(d) {
|
|
43
|
+
return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 本地自然周(周一 00:00 起)的毫秒时间戳。 */
|
|
47
|
+
function startOfWeek(d) {
|
|
48
|
+
const monday = new Date(d.getFullYear(), d.getMonth(), d.getDate())
|
|
49
|
+
monday.setDate(monday.getDate() - ((monday.getDay() + 6) % 7))
|
|
50
|
+
return monday.getTime()
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** 本地自然月 1 日 00:00 的毫秒时间戳。 */
|
|
54
|
+
function startOfMonth(d) {
|
|
55
|
+
return new Date(d.getFullYear(), d.getMonth(), 1).getTime()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** 按本地月序号(可为负,自动借位)取自然月起点。 */
|
|
59
|
+
function monthStartShift(d, shift) {
|
|
60
|
+
return new Date(d.getFullYear(), d.getMonth() + shift, 1).getTime()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** 从 d 往前/后 n 个日历月,锚定同一日与时分秒(月末溢出由 Date 自动进位,确定性)。 */
|
|
64
|
+
function nowShiftMonths(d, n) {
|
|
65
|
+
return new Date(d.getFullYear(), d.getMonth() + n, d.getDate(), d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds()).getTime()
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** 日期 d 所在月的下个月起点。 */
|
|
69
|
+
function nextMonthStart(d) {
|
|
70
|
+
return new Date(d.getFullYear(), d.getMonth() + 1, 1).getTime()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 解析查询文本中的中文时间表达。
|
|
75
|
+
*
|
|
76
|
+
* @param {string} text 查询文本
|
|
77
|
+
* @param {{now?:number}} [opts] now:毫秒时间戳(注入以保证确定性;缺省 Date.now())
|
|
78
|
+
* @returns {null | {startMs:number, endMs:number, matched:string}}
|
|
79
|
+
* null=未识别到时间表达(调用方零行为变更);否则为半开区间 [startMs, endMs)
|
|
80
|
+
* 与命中的原文子串 matched。多个表达命中时取**首个**(按下方模式表顺序,确定性)。
|
|
81
|
+
*/
|
|
82
|
+
export function parseTemporalQueryPre(text, opts = {}) {
|
|
83
|
+
try {
|
|
84
|
+
if (typeof text !== 'string' || !text) return null
|
|
85
|
+
const nowMs = Number.isFinite(opts.now) ? opts.now : Date.now()
|
|
86
|
+
const now = new Date(nowMs)
|
|
87
|
+
const today0 = startOfDay(now)
|
|
88
|
+
// 模式表(顺序即优先级:长/特异表达在前,防止子串误配,如"大前天"先于"前天"、"上上周"先于"上周")
|
|
89
|
+
const patterns = [
|
|
90
|
+
{ re: /大前天/, fn: () => ({ startMs: today0 - 3 * DAY_MS, endMs: today0 - 2 * DAY_MS }) },
|
|
91
|
+
{ re: /前天/, fn: () => ({ startMs: today0 - 2 * DAY_MS, endMs: today0 - DAY_MS }) },
|
|
92
|
+
{ re: /昨天/, fn: () => ({ startMs: today0 - DAY_MS, endMs: today0 }) },
|
|
93
|
+
{ re: /今天/, fn: () => ({ startMs: today0, endMs: today0 + DAY_MS }) },
|
|
94
|
+
{ re: /上上周/, fn: () => ({ startMs: startOfWeek(now) - 14 * DAY_MS, endMs: startOfWeek(now) - 7 * DAY_MS }) },
|
|
95
|
+
{ re: /上周/, fn: () => ({ startMs: startOfWeek(now) - 7 * DAY_MS, endMs: startOfWeek(now) }) },
|
|
96
|
+
{ re: /本周|这一周|这个星期/, fn: () => ({ startMs: startOfWeek(now), endMs: startOfWeek(now) + 7 * DAY_MS }) },
|
|
97
|
+
{ re: /上个月|上月/, fn: () => ({ startMs: monthStartShift(now, -1), endMs: startOfMonth(now) }) },
|
|
98
|
+
{ re: /本月|这个月/, fn: () => ({ startMs: startOfMonth(now), endMs: nextMonthStart(now) }) },
|
|
99
|
+
{ re: /去年/, fn: () => ({ startMs: new Date(now.getFullYear() - 1, 0, 1).getTime(), endMs: new Date(now.getFullYear(), 0, 1).getTime() }) },
|
|
100
|
+
{ re: /今年/, fn: () => ({ startMs: new Date(now.getFullYear(), 0, 1).getTime(), endMs: new Date(now.getFullYear() + 1, 0, 1).getTime() }) },
|
|
101
|
+
// 最近 N 个自然周期:从 now 往前 N 个周期(半开区间 [now-N周期, now));月按日历月锚定 now 时刻
|
|
102
|
+
{ re: /最近\s*([0-9零〇一二两三四五六七八九十百]+)\s*个?\s*月/, fn: (m) => {
|
|
103
|
+
const n = numToInt(m[1]); if (!Number.isInteger(n) || n < N_MIN || n > N_MAX) return null
|
|
104
|
+
return { startMs: nowShiftMonths(now, -n), endMs: nowMs }
|
|
105
|
+
} },
|
|
106
|
+
{ re: /最近\s*([0-9零〇一二两三四五六七八九十百]+)\s*个?\s*(?:星期|周)/, fn: (m) => {
|
|
107
|
+
const n = numToInt(m[1]); if (!Number.isInteger(n) || n < N_MIN || n > N_MAX) return null
|
|
108
|
+
return { startMs: nowMs - n * 7 * DAY_MS, endMs: nowMs }
|
|
109
|
+
} },
|
|
110
|
+
{ re: /最近\s*([0-9零〇一二两三四五六七八九十百]+)\s*(?:天|日)/, fn: (m) => {
|
|
111
|
+
const n = numToInt(m[1]); if (!Number.isInteger(n) || n < N_MIN || n > N_MAX) return null
|
|
112
|
+
return { startMs: nowMs - n * DAY_MS, endMs: nowMs }
|
|
113
|
+
} },
|
|
114
|
+
// N 个月前:定位到那个自然月(整月)
|
|
115
|
+
{ re: /([0-9零〇一二两三四五六七八九十百]+)\s*个?\s*月前/, fn: (m) => {
|
|
116
|
+
const n = numToInt(m[1]); if (!Number.isInteger(n) || n < N_MIN || n > N_MAX) return null
|
|
117
|
+
return { startMs: monthStartShift(now, -n), endMs: monthStartShift(now, -n + 1) }
|
|
118
|
+
} },
|
|
119
|
+
// N 周(星期)前:定位到那个自然周(周一 00:00 起整周)
|
|
120
|
+
{ re: /([0-9零〇一二两三四五六七八九十百]+)\s*个?\s*(?:星期|周)前/, fn: (m) => {
|
|
121
|
+
const n = numToInt(m[1]); if (!Number.isInteger(n) || n < N_MIN || n > N_MAX) return null
|
|
122
|
+
const base = startOfWeek(now) - n * 7 * DAY_MS
|
|
123
|
+
return { startMs: base, endMs: base + 7 * DAY_MS }
|
|
124
|
+
} },
|
|
125
|
+
// N 天(日)前:定位到那个自然日 00:00–24:00
|
|
126
|
+
{ re: /([0-9零〇一二两三四五六七八九十百]+)\s*(?:天|日)前/, fn: (m) => {
|
|
127
|
+
const n = numToInt(m[1]); if (!Number.isInteger(n) || n < N_MIN || n > N_MAX) return null
|
|
128
|
+
const base = today0 - n * DAY_MS
|
|
129
|
+
return { startMs: base, endMs: base + DAY_MS }
|
|
130
|
+
} },
|
|
131
|
+
]
|
|
132
|
+
for (const p of patterns) {
|
|
133
|
+
const m = p.re.exec(text)
|
|
134
|
+
if (!m) continue
|
|
135
|
+
const range = p.fn(m)
|
|
136
|
+
if (!range) continue
|
|
137
|
+
return { startMs: range.startMs, endMs: range.endMs, matched: m[0] }
|
|
138
|
+
}
|
|
139
|
+
return null
|
|
140
|
+
} catch (_) {
|
|
141
|
+
return null
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* 从 L0 候选 label 抽取候选时间戳(当日 00:00 本地)。
|
|
147
|
+
* label 形如日志/反思文件名(含 YYYY-MM-DD,如 "2026-09-09.md"、"reflections/2026-08-19.md");
|
|
148
|
+
* 非日期来源(".../MEMORY.md"、"~/MEMORY.md")→ null → 时间臂对该候选不参与(中性)。
|
|
149
|
+
*/
|
|
150
|
+
export function labelToDateMsPre(label) {
|
|
151
|
+
try {
|
|
152
|
+
const m = /(\d{4})-(\d{2})-(\d{2})/.exec(String(label || ''))
|
|
153
|
+
if (!m) return null
|
|
154
|
+
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]))
|
|
155
|
+
return Number.isFinite(d.getTime()) ? d.getTime() : null
|
|
156
|
+
} catch (_) {
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@a9i5k4/dsh-auto-memory",
|
|
3
3
|
"description": "Proactive associative memory for DSH: zero-prompt recall injected before the model speaks, three-layer auto-consolidation, skill crystallization, and Astra-style context management - handoff ledgers, PLAN whiteboard, water-level sensing. Local-first, model-agnostic, zero deps. 主动联想记忆+Astra 式上下文管理:自动唤回/自动沉淀/技能固化/交接账本与白板跨窗口续命/水位感知。",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -959,11 +959,35 @@ class SemanticWorker(base.Worker):
|
|
|
959
959
|
import traceback as _tb
|
|
960
960
|
import sys as _sys
|
|
961
961
|
try:
|
|
962
|
+
# P13:recall_rank 在 base dispatch(worker_v1.handle_frame)白名单外,在此拦截。
|
|
963
|
+
if req.get('type') == 'recall_rank':
|
|
964
|
+
return self.handle_recall_rank(req)
|
|
962
965
|
return super().handle_frame(req)
|
|
963
966
|
except Exception:
|
|
964
967
|
_sys.stderr.write('[fv2-trace] ' + _tb.format_exc() + '\n')
|
|
965
968
|
raise
|
|
966
969
|
|
|
970
|
+
def handle_recall_rank(self, req):
|
|
971
|
+
"""P13:recall 的 C3 语义臂 —— 只读 dense_search(三重过滤:workspaceRef+scope+miv),
|
|
972
|
+
不写回、不改索引、不新造存储;embedder 未就绪/无 vectors/encode 失败/任何异常
|
|
973
|
+
→ scores=[] fail-soft(不抛,JS 侧据此回退 C2→词法)。"""
|
|
974
|
+
p = req.get('payload') or {}
|
|
975
|
+
miv = str(p.get('memoryIndexVersion', p.get('miv', '')))
|
|
976
|
+
scores = []
|
|
977
|
+
try:
|
|
978
|
+
hits = self.dense_search(
|
|
979
|
+
str(p.get('query', '')),
|
|
980
|
+
str(p.get('workspaceKey', '')),
|
|
981
|
+
str(p.get('scope', 'Workspace')),
|
|
982
|
+
miv,
|
|
983
|
+
top_k=max(1, min(64, int(p.get('topK') or 20))),
|
|
984
|
+
)
|
|
985
|
+
scores = [{'memoryId': h['memoryId'], 'score': h['score']} for h in hits]
|
|
986
|
+
except Exception as exc: # noqa: BLE001
|
|
987
|
+
base.diag('recall_rank-failed: ' + str(exc))
|
|
988
|
+
return [self._frame(req, 'recall_rank_result',
|
|
989
|
+
{'scores': scores, 'miv': miv})]
|
|
990
|
+
|
|
967
991
|
def handle_close_session(self, req):
|
|
968
992
|
p = req.get('payload') or {}
|
|
969
993
|
sid = str(p.get('sessionId', ''))
|
|
@@ -1294,7 +1318,10 @@ def run_loop(worker):
|
|
|
1294
1318
|
'sentAt': obj.get('sentAt', 0)}
|
|
1295
1319
|
except (UnicodeDecodeError, ValueError):
|
|
1296
1320
|
obj = None
|
|
1297
|
-
|
|
1321
|
+
# P13:recall_rank(JS→PY 新请求类型)在 base.envelope_shape_ok 的 JS_TYPES 白名单外,
|
|
1322
|
+
# 在此显式放行(其余字段仍按协议帧校验);handler 在 SemanticWorker.handle_frame 拦截。
|
|
1323
|
+
obj_type_ok = isinstance(obj, dict) and obj.get('type') == 'recall_rank'
|
|
1324
|
+
if not isinstance(obj, dict) or not (obj_type_ok or base.envelope_shape_ok(obj)):
|
|
1298
1325
|
out.write((base.dumps(worker.error_frame(req_for_error,
|
|
1299
1326
|
'invalid-envelope')) + '\n').encode('utf-8'))
|
|
1300
1327
|
out.flush()
|