@a9i5k4/dsh-auto-memory 3.0.0 → 3.1.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 +30 -13
- package/README.zh-CN.md +30 -13
- package/docs/FRONTEND-CO-CREATION.md +191 -0
- package/docs/GM53-HOMEPAGE-PROMPT.md +323 -0
- package/docs/HANDBOOK.md +88 -52
- package/docs/HOMEPAGE-CONTENT-FOR-GM53.md +299 -0
- package/docs/PROMO-PROMPT-3.0.md +100 -0
- package/docs/USER-GUIDE.en.md +11 -11
- package/docs/USER-GUIDE.zh-CN.md +11 -11
- package/docs/WHITEPAPER.md +207 -0
- package/docs/screenshots/promo/promo-0-banner-v3.png +0 -0
- package/docs/screenshots/promo/promo-0-banner-v4.png +0 -0
- package/docs/screenshots/promo/promo-1b-auto-recall.png +0 -0
- package/lib/activation-host.js +69 -10
- package/lib/board-mode.js +1 -1
- package/lib/client.js +1697 -285
- package/lib/config-io.js +156 -0
- package/lib/context-bridge.js +3 -0
- package/lib/context-host.js +23 -10
- package/lib/degrade.js +385 -0
- package/lib/dsh-home.js +143 -0
- package/lib/episodic-store.js +142 -18
- package/lib/evidence-store.js +8 -1
- package/lib/fact-store.js +484 -43
- package/lib/hub-io.js +217 -0
- package/lib/index-sync.js +13 -1
- package/lib/index.js +1730 -202
- package/lib/intent-clean-safe.js +258 -40
- package/lib/l0-extract.js +231 -16
- package/lib/m4-corpus.js +8 -2
- package/lib/m7-index-sync-host.js +8 -1
- package/lib/memory-envelope.js +6 -1
- package/lib/memory-hub.js +164 -17
- package/lib/memory-index.js +4 -2
- package/lib/note-status-apply.js +118 -0
- package/lib/note-status.js +204 -0
- package/lib/procedure-store.js +333 -31
- package/lib/procedure-switch.js +38 -0
- package/lib/python-sidecar-client.js +314 -11
- package/lib/recall-fusion.js +83 -12
- package/lib/rules-edit.js +159 -0
- package/lib/semantic-decide.js +41 -8
- package/lib/semantic-js.js +51 -6
- package/lib/shadow-host.js +3 -5
- package/lib/skill-export-host.js +153 -0
- package/lib/skill-export.js +239 -0
- package/lib/storage-manage.js +6 -0
- package/lib/temporal-parse.js +191 -159
- package/lib/tier0-catalog.js +45 -3
- package/lib/wb-contract.js +198 -2
- package/lib/wb-sidecar.js +54 -3
- package/package.json +6 -2
- package/docs/internal/ACCEPT-35-LIVE.md +0 -143
- package/docs/internal/ACCEPTANCE-20260914.md +0 -90
- package/docs/internal/ARCH-REVIEW-BRIEF.md +0 -411
- package/docs/internal/ARCH-REVIEW-REQUEST.md +0 -201
- package/docs/internal/ARCH-REVIEW-ROUND2.md +0 -169
- package/docs/internal/ARCH-REVIEW-ROUND3.md +0 -206
- package/docs/internal/ART-DIRECTION-WIREFRAME.md +0 -181
- package/docs/internal/AUDIT-WB-GRAPH-FULL-20260916.md +0 -314
- package/docs/internal/CONCURRENCY-INVESTIGATION-20260917.md +0 -192
- package/docs/internal/CROSS-SESSION-SEARCH-PATH-DECISION.md +0 -72
- package/docs/internal/CROSS-SESSION-SEARCH-RESEARCH.md +0 -131
- package/docs/internal/CUA-VISION-FIX-NOTES.md +0 -78
- package/docs/internal/DECISIONS-20260914-SESSION.md +0 -269
- package/docs/internal/DESIGN-OVERHAUL-PRE-RESEARCH.md +0 -292
- package/docs/internal/DESIGN-P1-STATE-COMMIT-20260915.md +0 -219
- package/docs/internal/DIRECTION-CHECK-WB-GRAPH-20260916.md +0 -132
- package/docs/internal/FEEDBACK-TO-DSHAPI-RELAY.md +0 -13
- package/docs/internal/GH-DISCUSSION-5732-COMMENT.md +0 -74
- package/docs/internal/GPT-ACCEPTANCE-PROMPT-20260916.md +0 -352
- package/docs/internal/GPT-REVIEW-PROMPT.md +0 -216
- package/docs/internal/GROUP-DIGEST-SETUP.md +0 -62
- package/docs/internal/GROUP-LISTENER-SETUP.md +0 -49
- package/docs/internal/GROUP-WEBHOOK-SETUP.md +0 -93
- package/docs/internal/HANDOFF-TO-ZCODE.md +0 -168
- package/docs/internal/KICKOFF-P0.md +0 -254
- package/docs/internal/MASTER-PLAN-3.0.md +0 -411
- package/docs/internal/MEMORY-MUTATION-AND-INDEX-DESIGN.md +0 -85
- package/docs/internal/MERGE-CONFLICT-SCAN-20260914.md +0 -222
- package/docs/internal/NEXT-VERSION-TODO.md +0 -95
- package/docs/internal/OFFICIAL-DISCUSSION-DRAFT.md +0 -80
- package/docs/internal/PENDING-FIXES-20260916.md +0 -289
- package/docs/internal/RAG-KARPATHY-PROGRAM.md +0 -229
- package/docs/internal/RELEASE-PROCESS.md +0 -99
- package/docs/internal/REPORT-P0-NIGHTLY.md +0 -212
- package/docs/internal/REPORT-P5-ACCEPTANCE.md +0 -31
- package/docs/internal/REPORT-WB-GRAPH-NIGHTLY.md +0 -153
- package/docs/internal/REVIEW-WB-GRAPH-SELF.md +0 -81
- package/docs/internal/ROADMAP-20260917-WEEK.md +0 -305
- package/docs/internal/ROADMAP.md +0 -106
- package/docs/internal/RUN-P0-NIGHTLY.md +0 -227
- package/docs/internal/S10-CONSTRUCTION-HANDOFF-20260917.md +0 -175
- package/docs/internal/S10-GAPS-PLAIN-20260917.md +0 -125
- package/docs/internal/SEMANTIC-ARCHITECTURE-SPEC.md +0 -360
- package/docs/internal/SESSION-FILE-REPAIR-PROTOCOL.md +0 -90
- package/docs/internal/SUBAGENT-REPORT-ROUTING-PRE-RESEARCH.md +0 -261
- package/docs/internal/THREE-LAYER-CONTRACT.md +0 -210
- package/docs/internal/TODO-BACKLOG.md +0 -263
- package/docs/internal/TODO-GRAPH.html +0 -715
- package/docs/internal/TODO-GRAPH.html.bak-20260914-v2 +0 -493
- package/docs/internal/TODO-GRAPH.html.bak-20260915-alsfix +0 -710
- package/docs/internal/TODO-GRAPH.html.bak-20260915-p1 +0 -710
- package/docs/internal/TODO-GRAPH.html.bak-20260915-p6a-rev +0 -703
- package/docs/internal/TODO-GRAPH.html.bak-20260915-wshint +0 -710
- package/docs/internal/TODO-GRAPH.html.bak-20260916-batch +0 -715
- package/docs/internal/WB-FORMAT-CONVENTION.md +0 -112
- package/docs/internal/WB-GRAPH-DECISIONS-20260914.md +0 -71
- package/docs/internal/WB-GRAPH-INTEGRATION-PLAN.md +0 -386
- package/docs/internal/WB-GRAPH-RESEARCH-BRIEF.md +0 -118
- package/docs/internal/WB-GRAPH-RESEARCH-EXTERNAL.md +0 -228
- package/docs/internal/WB-GRAPH-RESEARCH-LOCAL.md +0 -190
- package/docs/internal/reviews/CLAIM-VERIFICATION-20260914.md +0 -56
- package/docs/internal/reviews/PLAN-gpt6astra-round2-20260914.md +0 -787
- package/docs/internal/reviews/REVIEW-gpt6astra-20260914.md +0 -112
- package/docs/internal/reviews/ROUND3-REVIEW-INTEGRATION-20260914.md +0 -230
package/lib/tier0-catalog.js
CHANGED
|
@@ -46,6 +46,19 @@ import { readFileSync, statSync } from 'node:fs'
|
|
|
46
46
|
|
|
47
47
|
export const TIER0_CATALOG_VERSION = 'tier0_catalog_v1'
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* ★M1(2026-09-18):白板锚点 id 的**形态判据**(契约 §2)。
|
|
51
|
+
*
|
|
52
|
+
* 与 `wb-sidecar.js:466 WB_ANCHOR_LINE_RE_V1`、契约 §2 的
|
|
53
|
+
* `^mem_[0-9a-f]{32}$` **同源同形**。此处**本地定义而非 import**:
|
|
54
|
+
* - tier0-catalog 是本仓最底层的目录渲染模块,不应反向依赖 wb-sidecar(避免耦合/循环);
|
|
55
|
+
* - 两处形态由**同一份契约文档**约束,套件断言两者**逐字相同**(防双源漂移)。
|
|
56
|
+
*
|
|
57
|
+
* 用途:**只在匹配时才走契约 §3 的链接形态**;id 形态非法则退回纯文本
|
|
58
|
+
* —— fail-closed:宁可输出纯文本,也不产出**指向不存在锚点的坏链接**。
|
|
59
|
+
*/
|
|
60
|
+
export const TIER0_ANCHOR_ID_RE = /^mem_[0-9a-f]{32}$/
|
|
61
|
+
|
|
49
62
|
export const TIER0_DEFAULTS = Object.freeze({
|
|
50
63
|
maxTokens: 800,
|
|
51
64
|
oneLineChars: 80,
|
|
@@ -312,17 +325,46 @@ function extractSourcePre(text, layer, ctx, cfg) {
|
|
|
312
325
|
return { items, units: groups.length, dropped }
|
|
313
326
|
}
|
|
314
327
|
|
|
315
|
-
/**
|
|
328
|
+
/**
|
|
329
|
+
* 一条目录项 → 一行文本。**导出**:渲染口径的唯一实现。
|
|
330
|
+
*
|
|
331
|
+
* ★M1 契约行渲染(2026-09-18):**同一次派生、两种渲染**。
|
|
332
|
+
* - 无 `path`/`id` ⇒ 纯文本(**逐字节同旧实现**,老调用方与老套件零影响)
|
|
333
|
+
* - 有 `path`+`id` ⇒ 契约 §3 链接形态:
|
|
334
|
+
* `- [标题](<页面路径>#mem_<32hex>) — 一句话摘要 · layer=whiteboard · status=current · 日期`
|
|
335
|
+
*
|
|
336
|
+
* 为什么是"两种渲染"而不是"新文件":契约 §8 要求
|
|
337
|
+
* 「由页面派生出的 index 与 Tier-0 目录条目**一致**」——
|
|
338
|
+
* 两者出自**同一份 items**、同一个渲染函数,一致性**天然成立**,不需要额外同步机制。
|
|
339
|
+
* (新建独立文件会新增状态源,违反 S10.4「不建状态机」。)
|
|
340
|
+
*/
|
|
316
341
|
export function renderCatalogLinePre(item) {
|
|
317
342
|
const it = item && typeof item === 'object' ? item : {}
|
|
318
343
|
const title = clean(it.title)
|
|
319
344
|
const oneLine = clean(it.oneLine)
|
|
320
345
|
const date = clean(it.date)
|
|
346
|
+
const layer = clean(it.layer) || 'log'
|
|
347
|
+
const status = clean(it.status) || 'current'
|
|
348
|
+
|
|
349
|
+
// ── 契约 §3 链接形态(仅当 path + id 齐备时启用)──
|
|
350
|
+
const p = clean(it.path)
|
|
351
|
+
const id = clean(it.id)
|
|
352
|
+
if (p && id && TIER0_ANCHOR_ID_RE.test(id)) {
|
|
353
|
+
const head = title || oneLine
|
|
354
|
+
const link = '[' + head + '](' + p + '#' + id + ')'
|
|
355
|
+
const segs = [link]
|
|
356
|
+
// 摘要:与标题不同才追加(同旧实现口径:title===oneLine 时不重复)
|
|
357
|
+
if (oneLine && oneLine !== title) segs.push(oneLine)
|
|
358
|
+
const line = segs.join(' — ')
|
|
359
|
+
return '- ' + line + ' · layer=' + layer + ' · status=' + status + (date ? ' · ' + date : '')
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── 纯文本形态(逐字节同旧实现)──
|
|
321
363
|
const segs = []
|
|
322
364
|
if (title && title !== oneLine) segs.push(title)
|
|
323
365
|
if (oneLine) segs.push(oneLine)
|
|
324
|
-
segs.push(
|
|
325
|
-
segs.push(
|
|
366
|
+
segs.push(layer)
|
|
367
|
+
segs.push(status)
|
|
326
368
|
if (date) segs.push(date)
|
|
327
369
|
return segs.join(' · ')
|
|
328
370
|
}
|
package/lib/wb-contract.js
CHANGED
|
@@ -42,7 +42,12 @@ export const WB_MARKERS_V1 = Object.freeze({
|
|
|
42
42
|
userClose: '<!-- /user -->',
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
-
/** 卡片状态(与记忆条目状态同域;缺 `status` 视为 `current`,与 `isCurrentPre` 口径一致)。
|
|
45
|
+
/** 卡片状态(与记忆条目状态同域;缺 `status` 视为 `current`,与 `isCurrentPre` 口径一致)。
|
|
46
|
+
* ⚠️ 2026-09-17 L7 存活标注: 本常量**当前无仓内消费者**(全仓引用计数 = 1, 仅此定义)。
|
|
47
|
+
* 判定 = **对外契约保留**, 不删 —— 理由是它是 WB-FORMAT-CONVENTION 的**状态值域声明**
|
|
48
|
+
* (`current`/`superseded`/`retracted`), 属已发布插件 (@a9i5k4/dsh-auto-memory 3.0.0, 有真实用户)
|
|
49
|
+
* 的公开导出面; 第三方按契约读取状态时需要它作为**单一真源**, 删掉等于把值域契约降级成散落字面量。
|
|
50
|
+
* ⇒ 保留 + 显式标注, 不做"沉默留着"。 */
|
|
46
51
|
export const WB_STATUSES_V1 = Object.freeze(['current', 'superseded', 'retracted'])
|
|
47
52
|
|
|
48
53
|
/** 解析/校验的原因码 → 可读中文。 */
|
|
@@ -59,7 +64,11 @@ export const WB_REASONS_V1 = Object.freeze({
|
|
|
59
64
|
'user-region-interleaved': '人机分区标记交错(user 段里又开 model 段)',
|
|
60
65
|
})
|
|
61
66
|
|
|
62
|
-
/** 原因码 → 可读中文(未知码原样返回,排障不吞信息)。
|
|
67
|
+
/** 原因码 → 可读中文(未知码原样返回,排障不吞信息)。
|
|
68
|
+
* ⚠️ 2026-09-17 L7 存活标注: 本函数**当前无仓内调用者**(全仓引用计数 = 1, 仅此定义)。
|
|
69
|
+
* 判定 = **对外契约保留**, 不删 —— 它是 `WB_REASONS_V1`(上文, **有消费者**) 的唯一
|
|
70
|
+
* 取值入口; 删掉函数会让那份码表的"怎么读"离开公开面, 而码表本身是契约的一部分
|
|
71
|
+
* (发布版已有真实用户)。⇒ 保留 + 显式标注。 */
|
|
63
72
|
export function describeWbReasonPre(code) {
|
|
64
73
|
const k = String(code == null ? '' : code)
|
|
65
74
|
return WB_REASONS_V1[k] || k || '未知原因'
|
|
@@ -254,6 +263,193 @@ function emptyResult(kind, issues) {
|
|
|
254
263
|
}
|
|
255
264
|
}
|
|
256
265
|
|
|
266
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
267
|
+
// ★M2(2026-09-18)白板 lint —— 契约 §6「零 token 可判的四类」
|
|
268
|
+
//
|
|
269
|
+
// **硬约束(契约 §6 尾注,不可越)**:lint **只报告,不自动改**。
|
|
270
|
+
// 「任何自动修正都会把白板变成状态机(违反边界)」
|
|
271
|
+
// ⇒ 本模块是**纯函数**:不读文件、不写文件、不 import fs。
|
|
272
|
+
// 输入什么就判什么,输出问题清单。任何写盘能力都不得塞进这里(报告与写入分离)。
|
|
273
|
+
//
|
|
274
|
+
// 四类判据(§6 原文)与**本实现的口径**(判据纪律:宁可漏判,不可误伤):
|
|
275
|
+
// ① 孤立条目:无入站引用 → 全部卡正文里都找不到它的 id/标题
|
|
276
|
+
// ② 陈旧:status≠current 或超期 current → 状态直读 + 与 now 比日期
|
|
277
|
+
// ③ 被提及却无独立卡 → ★ 只判**客观可证**的一种:正文引用了
|
|
278
|
+
// `mem_<32hex>` 但该 id 没有对应卡片("什么算概念"无法零 token 判定,不猜)
|
|
279
|
+
// ④ 缺交叉引用 → ★ 只判**客观可证**的一种:两卡共享 ≥N 个 tag
|
|
280
|
+
// 却互不引用("什么算相关"由 tag 交集代言,不猜语义)
|
|
281
|
+
// ⑤ 矛盾检测 → 需 LLM,**不在本模块**(契约 §6:必须手动触发,不进自动路径)
|
|
282
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
283
|
+
|
|
284
|
+
export const WB_LINT_VERSION = 'wb_lint_v1'
|
|
285
|
+
|
|
286
|
+
/** lint 问题码(§6 四类;⑤ 矛盾检测需 LLM,不在此列)。 */
|
|
287
|
+
export const WB_LINT_CODES_V1 = Object.freeze({
|
|
288
|
+
ORPHAN: 'orphan', // ① 孤立条目
|
|
289
|
+
INACTIVE: 'inactive', // ② 陈旧(已标 superseded/retracted)
|
|
290
|
+
EXPIRED: 'expired', // ② 陈旧(仍标 current 但超阈值)
|
|
291
|
+
DANGLING_REF: 'dangling-ref', // ③ 被引用却无卡(悬空锚点引用)
|
|
292
|
+
NO_CROSS_REF: 'no-cross-ref', // ④ 缺交叉引用(共享 tag 却互不引用)
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
/** 默认口径。 */
|
|
296
|
+
export const WB_LINT_DEFAULTS_V1 = Object.freeze({
|
|
297
|
+
staleDays: 90, // ② 仍标 current 但日期早于 now-90d ⇒ 报 expired
|
|
298
|
+
minSharedTags: 2, // ④ 共享 tag 数 ≥ 2 才算"应互链"
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
const LINT_ANCHOR_RE = /mem_[0-9a-f]{32}/g
|
|
302
|
+
|
|
303
|
+
const asDateMs = (v) => {
|
|
304
|
+
const s = String(v == null ? '' : v)
|
|
305
|
+
const m = s.match(/^(\d{4})-(\d{2})-(\d{2})/)
|
|
306
|
+
if (!m) return NaN
|
|
307
|
+
const t = Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]))
|
|
308
|
+
return Number.isFinite(t) ? t : NaN
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* 白板 lint(**只读纯函数**,契约 §6)。
|
|
313
|
+
*
|
|
314
|
+
* @param {Array<object>|{entries?:Array<object>}} entries 条目数组;每项可含
|
|
315
|
+
* `{id, title, status, date, ts, tags, body}`。`body` 为该卡正文(用于扫描引用);
|
|
316
|
+
* 缺 `body` 时退化为用 `title+id` 判引用(**不报错**——信息少就少判,不猜)。
|
|
317
|
+
* @param {{now?:Date|string|number, staleDays?:number, minSharedTags?:number, corpus?:string}} [opts]
|
|
318
|
+
* `corpus` 为白板之外的**额外文本**(如项目笔记/账本全文),一并参与"入站引用"扫描。
|
|
319
|
+
* @returns {{version:string, ok:boolean, findings:Array<object>, counts:object}}
|
|
320
|
+
*/
|
|
321
|
+
export function lintWhiteboardPre(entries, opts = {}) {
|
|
322
|
+
const o = opts && typeof opts === 'object' ? opts : {}
|
|
323
|
+
const list = Array.isArray(entries) ? entries
|
|
324
|
+
: (entries && Array.isArray(entries.entries) ? entries.entries : [])
|
|
325
|
+
const staleDays = Number.isFinite(Number(o.staleDays)) ? Number(o.staleDays) : WB_LINT_DEFAULTS_V1.staleDays
|
|
326
|
+
const minSharedTags = Number.isFinite(Number(o.minSharedTags)) ? Number(o.minSharedTags) : WB_LINT_DEFAULTS_V1.minSharedTags
|
|
327
|
+
|
|
328
|
+
const nowMs0 = o.now instanceof Date ? o.now.getTime()
|
|
329
|
+
: (typeof o.now === 'number' ? o.now : asDateMs(o.now))
|
|
330
|
+
const nowMs = Number.isFinite(nowMs0) ? nowMs0 : Date.now()
|
|
331
|
+
|
|
332
|
+
// ── 归一化(**永不抛**:坏条目降级为跳过,不打断整轮 lint)──
|
|
333
|
+
const cards = []
|
|
334
|
+
let skipped = 0
|
|
335
|
+
for (const e of list) {
|
|
336
|
+
if (!e || typeof e !== 'object') { skipped++; continue }
|
|
337
|
+
const id = String(e.id || '')
|
|
338
|
+
const title = String(e.title || '').trim()
|
|
339
|
+
if (!id && !title) { skipped++; continue }
|
|
340
|
+
const status = String(e.status || 'current').trim() || 'current'
|
|
341
|
+
const dateSrc = e.date || e.ts || ''
|
|
342
|
+
const tags = Array.isArray(e.tags) ? e.tags.map((t) => String(t)).filter(Boolean) : []
|
|
343
|
+
cards.push({ id, title, status, tags, dateMs: asDateMs(dateSrc), dateRaw: String(dateSrc || '') })
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ── 引用语料:所有卡正文 + 外部 corpus ──
|
|
347
|
+
const parts = []
|
|
348
|
+
for (const e of list) {
|
|
349
|
+
if (e && typeof e === 'object') {
|
|
350
|
+
if (e.body != null) parts.push(String(e.body))
|
|
351
|
+
if (e.preview != null) parts.push(String(e.preview))
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
if (o.corpus != null) parts.push(String(o.corpus))
|
|
355
|
+
const haystack = parts.join('\n')
|
|
356
|
+
|
|
357
|
+
const findings = []
|
|
358
|
+
const push = (code, card, detail) => {
|
|
359
|
+
findings.push({ code, id: card ? card.id : '', title: card ? card.title : '', detail })
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── ① 孤立条目:无入站引用 ──
|
|
363
|
+
// 判据:除自己以外,任何卡正文/外部语料都未出现它的 id,也未出现它的标题。
|
|
364
|
+
// 纪律:**只报"全无提及"**;标题过短(<2 字符)时跳过,避免单字误命中(宁可漏判)。
|
|
365
|
+
for (const c of cards) {
|
|
366
|
+
let referenced = false
|
|
367
|
+
if (c.id) {
|
|
368
|
+
const re = new RegExp(c.id.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')
|
|
369
|
+
const hits = haystack.match(re)
|
|
370
|
+
// 自己正文里出现自己的 id 不算入站引用
|
|
371
|
+
const selfBody = String((list.find((x) => x && x.id === c.id) || {}).body || '')
|
|
372
|
+
const selfHits = selfBody.match(re)
|
|
373
|
+
const inHits = (hits ? hits.length : 0) - (selfHits ? selfHits.length : 0)
|
|
374
|
+
if (inHits > 0) referenced = true
|
|
375
|
+
}
|
|
376
|
+
if (!referenced && c.title && c.title.length >= 2) {
|
|
377
|
+
if (haystack.includes(c.title)) {
|
|
378
|
+
// 标题出现次数 > 自身正文里的次数 ⇒ 有外部提及
|
|
379
|
+
const selfBody = String((list.find((x) => x && x.id === c.id) || {}).body || '')
|
|
380
|
+
const total = haystack.split(c.title).length - 1
|
|
381
|
+
const selfN = selfBody.split(c.title).length - 1
|
|
382
|
+
if (total - selfN > 0) referenced = true
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
if (!referenced) push(WB_LINT_CODES_V1.ORPHAN, c, '无任何其他卡片或语料引用它的 id/标题')
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ── ② 陈旧 ──
|
|
389
|
+
const cutoff = nowMs - staleDays * 86400000
|
|
390
|
+
for (const c of cards) {
|
|
391
|
+
if (c.status !== 'current') {
|
|
392
|
+
push(WB_LINT_CODES_V1.INACTIVE, c, 'status=' + c.status + '(已作废,仍在白板内)')
|
|
393
|
+
continue
|
|
394
|
+
}
|
|
395
|
+
if (Number.isFinite(c.dateMs) && c.dateMs < cutoff) {
|
|
396
|
+
const ageDays = Math.floor((nowMs - c.dateMs) / 86400000)
|
|
397
|
+
push(WB_LINT_CODES_V1.EXPIRED, c, '仍标 current 但日期为 ' + c.dateRaw + '(' + ageDays + ' 天前,超阈值 ' + staleDays + ' 天)')
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// ── ③ 悬空锚点引用:正文引用了 mem_id 但没有对应卡片 ──
|
|
402
|
+
// 契约 §2 的 id 是"内容寻址、可复算"的 ⇒ 引用了不存在的 id 是**客观可证**的断裂。
|
|
403
|
+
const knownIds = new Set(cards.map((c) => c.id).filter(Boolean))
|
|
404
|
+
const allIds = new Set()
|
|
405
|
+
for (const m of haystack.match(LINT_ANCHOR_RE) || []) allIds.add(m)
|
|
406
|
+
for (const id of allIds) {
|
|
407
|
+
if (!knownIds.has(id)) {
|
|
408
|
+
findings.push({
|
|
409
|
+
code: WB_LINT_CODES_V1.DANGLING_REF, id, title: '',
|
|
410
|
+
detail: '正文引用了 ' + id + ',但白板内没有该 id 的卡片(悬空引用)',
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ── ④ 缺交叉引用:共享 ≥minSharedTags 个 tag 却互不引用 ──
|
|
416
|
+
for (let i = 0; i < cards.length; i++) {
|
|
417
|
+
for (let j = i + 1; j < cards.length; j++) {
|
|
418
|
+
const a = cards[i], b = cards[j]
|
|
419
|
+
if (!a.tags.length || !b.tags.length) continue
|
|
420
|
+
const shared = a.tags.filter((t) => b.tags.includes(t))
|
|
421
|
+
if (shared.length < minSharedTags) continue
|
|
422
|
+
const aRefB = (a.id && b.id && String((list.find((x) => x && x.id === a.id) || {}).body || '').includes(b.id)) ||
|
|
423
|
+
(b.title && String((list.find((x) => x && x.id === a.id) || {}).body || '').includes(b.title))
|
|
424
|
+
const bRefA = (a.id && b.id && String((list.find((x) => x && x.id === b.id) || {}).body || '').includes(a.id)) ||
|
|
425
|
+
(a.title && String((list.find((x) => x && x.id === b.id) || {}).body || '').includes(a.title))
|
|
426
|
+
if (!aRefB && !bRefA) {
|
|
427
|
+
findings.push({
|
|
428
|
+
code: WB_LINT_CODES_V1.NO_CROSS_REF, id: a.id, title: a.title,
|
|
429
|
+
detail: '与「' + (b.title || b.id) + '」共享 ' + shared.length + ' 个 tag(' + shared.join(', ') + ')但互不引用',
|
|
430
|
+
})
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const byCode = {}
|
|
436
|
+
for (const f of findings) byCode[f.code] = (byCode[f.code] || 0) + 1
|
|
437
|
+
|
|
438
|
+
return {
|
|
439
|
+
version: WB_LINT_VERSION,
|
|
440
|
+
ok: findings.length === 0,
|
|
441
|
+
findings,
|
|
442
|
+
counts: {
|
|
443
|
+
cards: cards.length,
|
|
444
|
+
skipped,
|
|
445
|
+
findings: findings.length,
|
|
446
|
+
byCode,
|
|
447
|
+
// ⑤ 明确声明"未判" —— 不许静默省略(契约 §6:需 LLM,必须手动触发)
|
|
448
|
+
notChecked: ['contradiction'],
|
|
449
|
+
},
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
257
453
|
/**
|
|
258
454
|
* 从投影里抽出"**必须逐字节保留**的用户区"(供写入门做前后比对)。
|
|
259
455
|
*
|
package/lib/wb-sidecar.js
CHANGED
|
@@ -287,8 +287,17 @@ export function buildSectionCardsPre(workspaceKey, docs, opts = {}) {
|
|
|
287
287
|
* 契约对齐 WB-GRAPH-INTEGRATION-PLAN §4.1(条目级): id/kind/source/section/tags/cues/preview/mtime/criteria。
|
|
288
288
|
* 兼容旧字段: title/cue/chars/ts 保留(既有套件与工具描述在用)。
|
|
289
289
|
*
|
|
290
|
+
* ★M1(2026-09-18)新增 `layer`/`status`:契约 §3 的索引派生行要求两者
|
|
291
|
+
* (`… · layer=whiteboard · status=current · 日期`)。此前 entry 只有 `id`(已是
|
|
292
|
+
* `mem_<32hex>` 形态),缺这两个字段 ⇒ 渲染侧拿不到值,一直退化成纯文本行。
|
|
293
|
+
* - `layer` 恒为 `'whiteboard'`:本模块**只服务于白板/账本**这一层,
|
|
294
|
+
* 不是从调用方传入的自由值(避免调用方各传各的、导致层级语义漂移)。
|
|
295
|
+
* - `status` 取 `o.status`,缺省 `'current'`(与契约 §3 尾注
|
|
296
|
+
* 「缺 status 视为 current」、与 `isCurrentPre` 口径一致)。
|
|
297
|
+
* 两个字段**只增不改**:既有字段顺序与语义逐字节不变。
|
|
298
|
+
*
|
|
290
299
|
* @param {{workspaceKey:string, relPath:string, text:string, title:string, ts?:string,
|
|
291
|
-
* kind?:string, mtime?:number, criteria?:string}} o
|
|
300
|
+
* kind?:string, mtime?:number, criteria?:string, status?:string}} o
|
|
292
301
|
*/
|
|
293
302
|
export function buildSidecarEntryPre(o = {}) {
|
|
294
303
|
const src = String(o.text || '')
|
|
@@ -308,6 +317,9 @@ export function buildSidecarEntryPre(o = {}) {
|
|
|
308
317
|
preview: src.replace(/\s+/g, ' ').trim().slice(0, 120),
|
|
309
318
|
mtime: Number(o.mtime) || 0,
|
|
310
319
|
criteria: String(o.criteria || 'unknown'),
|
|
320
|
+
// ── ★M1:契约 §3 索引派生行所需(只增不改)──
|
|
321
|
+
layer: 'whiteboard',
|
|
322
|
+
status: String(o.status || 'current'),
|
|
311
323
|
// ── 兼容旧套件/工具描述的字段 ──
|
|
312
324
|
title,
|
|
313
325
|
cue: cueLine.trim().slice(0, 120),
|
|
@@ -316,6 +328,45 @@ export function buildSidecarEntryPre(o = {}) {
|
|
|
316
328
|
}
|
|
317
329
|
}
|
|
318
330
|
|
|
331
|
+
/**
|
|
332
|
+
* ★M1(2026-09-18):sidecar 条目 → Tier-0 目录项(**契约 §8 的桥**)。
|
|
333
|
+
*
|
|
334
|
+
* 契约 §8 要求:「由页面派生出的 index 与 Tier-0 目录条目**一致**」。
|
|
335
|
+
* 实现方式 = **同一次派生、两种渲染** —— 本函数**只做字段映射,不复制任何逻辑**:
|
|
336
|
+
* entries 与 Tier-0 条目本就是同一份数据,映射后交给
|
|
337
|
+
* `renderCatalogLinePre` 渲染,一致性天然成立(无需额外同步机制)。
|
|
338
|
+
*
|
|
339
|
+
* 为什么不在此处直接渲染成字符串:渲染口径必须**只有一处实现**
|
|
340
|
+
* (`renderCatalogLinePre`),否则「两种渲染」会退化成「两个渲染器」,
|
|
341
|
+
* 长期必然漂移 —— 这正是 §8 想防的。
|
|
342
|
+
*
|
|
343
|
+
* 字段映射(只映射,不发明):
|
|
344
|
+
* - title ← entry.title
|
|
345
|
+
* - oneLine ← entry.preview(已是单行化 + 120 字截断,即「一句话摘要」)
|
|
346
|
+
* - layer ← entry.layer(本模块恒为 'whiteboard')
|
|
347
|
+
* - status ← entry.status(缺省 'current')
|
|
348
|
+
* - date ← entry.ts 的日期部分(无则空;不编造)
|
|
349
|
+
* - path ← entry.source(相对路径,渲染时拼锚点)
|
|
350
|
+
* - id ← entry.id(已是 mem_<32hex>)
|
|
351
|
+
*
|
|
352
|
+
* @param {object} entry buildSidecarEntryPre 的产物
|
|
353
|
+
* @returns {{title:string,oneLine:string,layer:string,status:string,date:string,path:string,id:string}}
|
|
354
|
+
*/
|
|
355
|
+
export function sidecarEntryToCatalogItemPre(entry) {
|
|
356
|
+
const e = entry && typeof entry === 'object' ? entry : {}
|
|
357
|
+
const ts = String(e.ts || '')
|
|
358
|
+
const dateOnly = (ts.match(/^\d{4}-\d{2}-\d{2}/) || [''])[0]
|
|
359
|
+
return {
|
|
360
|
+
title: String(e.title || ''),
|
|
361
|
+
oneLine: String(e.preview || ''),
|
|
362
|
+
layer: String(e.layer || 'whiteboard'),
|
|
363
|
+
status: String(e.status || 'current'),
|
|
364
|
+
date: dateOnly,
|
|
365
|
+
path: String(e.source || ''),
|
|
366
|
+
id: String(e.id || ''),
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
319
370
|
/** tag → 条目 id 倒排(§3.3 by_tag)。 */
|
|
320
371
|
export function buildByTagPre(entries) {
|
|
321
372
|
const byTag = {}
|
|
@@ -389,10 +440,10 @@ export function rebuildSidecarIndexPre(workspaceKey, docs) {
|
|
|
389
440
|
|
|
390
441
|
/** 生成 §4.1 的 hint 文案(工具返回值里引导模型向下走)。 */
|
|
391
442
|
function hintForExpand() {
|
|
392
|
-
return '可用
|
|
443
|
+
return '可用 memory_trace(id) 回溯该条目的 cue/tag/邻居与归档版本'
|
|
393
444
|
}
|
|
394
445
|
function hintForTrace() {
|
|
395
|
-
return '可用
|
|
446
|
+
return '可用 memory_expand(tag) 按 tag 展开同主题条目; neighbors 为同 tag/同 cue 的相邻条目'
|
|
396
447
|
}
|
|
397
448
|
|
|
398
449
|
/**
|
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": "3.
|
|
4
|
+
"version": "3.1.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
"docs",
|
|
16
16
|
"cordis.patch.yml",
|
|
17
17
|
"!python/bench",
|
|
18
|
-
"!python/__pycache__"
|
|
18
|
+
"!python/__pycache__",
|
|
19
|
+
"!docs/internal",
|
|
20
|
+
"!docs/**/*.bak",
|
|
21
|
+
"!docs/**/*.bak-*",
|
|
22
|
+
"!**/*.mjs.bak-*"
|
|
19
23
|
],
|
|
20
24
|
"dsh": {
|
|
21
25
|
"bundle": {
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
# #35 真机验收方案(停旧回合 → 仪式 → 真判据 → 新窗口)
|
|
2
|
-
|
|
3
|
-
> 目标:把 #35 的修复从**夹具级**(`smoke-test-autocont-host-pre.mjs` 95 断言)升级为**真机级**。
|
|
4
|
-
> 本文件给**新会话**执行;执行者不需要读原对话。改配置前先备份(用户级硬规则)。
|
|
5
|
-
|
|
6
|
-
## 0. 为什么必须换一个会话做
|
|
7
|
-
|
|
8
|
-
- 验收的**核心那一档**要求在「回合正在运行时点同意」——那会**终止当前回合**。
|
|
9
|
-
拿重要对话当靶子会打断它,所以:**另开一个新会话当"旧会话"被接续**,本对话不受影响。
|
|
10
|
-
- 新会话要够长、有实质内容(交接材料才有东西可带);空会话测不出材料质量。
|
|
11
|
-
|
|
12
|
-
## 1. 前置条件(**2026-09-14 解耦后已更新:只需开一个开关**)
|
|
13
|
-
|
|
14
|
-
> 历史注记:解耦前 `handoffEnabled=false` 会让 `checkWaterLevel` 提前返回 → `waterLevelModelKnown` 不写 → fail-closed 闸永不 arm,所以当时**必须同时开两个开关**。那处耦合已切除(`lib/index.js` 的 `checkWaterLevel` 早退、`armAutoContinue` 早退、`autoContinueState.enabled` 二次与运算、`buildContinueCarry` 的 `handoff disabled` 早退,共 4 处)。
|
|
15
|
-
|
|
16
|
-
这台机器的现行配置(`~/.dsh/dsh-auto-memory-pre.json`):
|
|
17
|
-
|
|
18
|
-
| 键 | 现值 | 现在的要求 |
|
|
19
|
-
|---|---|---|
|
|
20
|
-
| `autoContinueEnabled` | **false** | **必须临时改 true**——它是接续的唯一资格开关 |
|
|
21
|
-
| `handoffEnabled` | false | **可保持 false**,此时走「最小转写载体」;也可改 true 走完整两层载体(见步骤 1 的 A/B 跑法) |
|
|
22
|
-
| `autoContinueThreshold` | 0.75 | 正常会话很难自然达阈值,验收要临时降 |
|
|
23
|
-
|
|
24
|
-
**结论:只开 `autoContinueEnabled` 即可跑验收。** 而且**建议第一轮就保持白板关闭**——那条路径是这次解耦才第一次可用的(此前直接 hard-fail),顺带把解耦也验了。
|
|
25
|
-
|
|
26
|
-
## 2. 步骤
|
|
27
|
-
|
|
28
|
-
### 步骤 0 · 备份(必做)
|
|
29
|
-
```pwsh
|
|
30
|
-
Copy-Item "$env:USERPROFILE\.dsh\dsh-auto-memory-pre.json" "$env:USERPROFILE\.dsh\dsh-auto-memory-pre.json.bak-accept35" -Force
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
### 步骤 1 · 临时开开关 + 降阈值(**A/B 两轮**)
|
|
34
|
-
|
|
35
|
-
**A 轮(先跑,白板关闭 —— 验解耦后的最小载体)**:
|
|
36
|
-
```json
|
|
37
|
-
"autoContinueEnabled": true,
|
|
38
|
-
"autoContinueThreshold": 0.05
|
|
39
|
-
```
|
|
40
|
-
保持 `handoffEnabled: false` 不动。此轮新会话应拿到**转写包 + 近期线程**,且材料里带一句自述「未启用白板/账本」;仪式步骤会被**跳过**(日志 `reason:'handoff-disabled'`),这是**预期**,不是失败。
|
|
41
|
-
|
|
42
|
-
**B 轮(A 通过后再跑,白板打开 —— 验完整两层载体)**:
|
|
43
|
-
再把 `"handoffEnabled": true` 打开,重跑一次,观察步骤 ②③ 的仪式链路。
|
|
44
|
-
|
|
45
|
-
> 为什么降的是 `autoContinueThreshold` 而不是 `waterLevelWindowTokens`:改窗口会让**水位测量本身**失真,连带 advisory 与自动账本写入都变形;降阈值只动"什么时候弹卡"。
|
|
46
|
-
> 每轮改完**重启 dsh web**(注入面与开关在启动时读)。**注意冷却**:`autoContinueCooldownMinutes=30`,触发过一次后 30 分钟内不再弹——两轮之间要么等,要么把冷却临时调小。
|
|
47
|
-
|
|
48
|
-
### 步骤 2 · 造场景
|
|
49
|
-
在**新会话**里正常跑一两轮(有工具调用、有实质产出)。
|
|
50
|
-
- 注意:**首轮 pre-step 拿不到模型信息**(`request/header` 还没写),按 #33 的 fail-closed 设计,首轮的按比例 arm 会被拦,**推迟到该轮 turn-stopping**。所以卡片应在**第一轮结束后**出现,而不是开局。
|
|
51
|
-
- 若 10 分钟不弹卡:先查日志有没有 `auto-continue armed`,没有就回头查开关与 `modelKnown`(见 §4 排查)。
|
|
52
|
-
|
|
53
|
-
### 步骤 3 · 点「同意接续」并采日志
|
|
54
|
-
两种点法,验证深度不同——**建议先 A 后 B**:
|
|
55
|
-
|
|
56
|
-
- **A 档(先做,安全)**:**空闲时点**。验通 ①→④ 全链路与节拍;`cancel` 对空转回合是空操作。
|
|
57
|
-
- **B 档(核心,代价已知)**:**让一个回合跑着(多步工具调用/长输出),在运行中点同意**。这才是 issue 原现场,也是唯一能验「回合活跃时不再并行」的做法。**代价:该回合会被终止,未完成的回答落 `interrupted`。**
|
|
58
|
-
|
|
59
|
-
采集(改完配置重启后):
|
|
60
|
-
```pwsh
|
|
61
|
-
Get-Content "$env:USERPROFILE\.dsh\dsh-auto-memory-pre-diagnose.log" -Tail 200 |
|
|
62
|
-
Select-String 'auto-continue|ritual|prev-session|stopped|waited'
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
## 3. 判据(缺一不算过;第 3 条仅 B 轮适用,A 轮看 3′)
|
|
66
|
-
|
|
67
|
-
| # | 期望日志 | 含义 |
|
|
68
|
-
|---|---|---|
|
|
69
|
-
| 1 | `auto-continue armed: … awaitIdle=true …` | 闸放行了(能验到这条本身就说明 #33 的 fail-closed 没误杀) |
|
|
70
|
-
| 2 | **`auto-continue: stopped old turn sid=…`** | **① 新链路真的执行了**;若旧回合在跑,旧窗口应显示被打断 |
|
|
71
|
-
| 3 | **`host refresh ritual: … waited=updated`**(**仅 B 轮/白板开时适用**) | **②③ 因果闭合判据通过**(若显示 `waited=stamp-fallback` → 说明 `sc.inspect` 不可用、退到弱判据,**要当作半通过并单独记**) |
|
|
72
|
-
| 3′ | **A 轮(白板关)替代判据**:日志出现仪式的 `reason:'handoff-disabled'`(仪式按设计被跳过),**且**新会话交接材料里出现「未启用白板/账本」自述、**不含**磁盘上真实存在的 PLAN/账本正文 | 解耦生效:白板关 ≠ 不能接续;且降级是**显式**的,不是静默丢弃 |
|
|
73
|
-
| 4 | `prev-session pack built` → `auto-continue host-executed: … stopped=ok` | ④ 新会话建立;`stopped=ok` 与判据 2 互相印证 |
|
|
74
|
-
|
|
75
|
-
**反向断言(同样重要)**:
|
|
76
|
-
- 若 ④ 出现但**没有** 2 → 停旧回合没生效(或旧会话无 id,`stopped=no-old-session`),必须记下来;
|
|
77
|
-
- 若旧回合被打断后**新会话没建起来** → 交接失败,比"并行"更糟,立即回滚并报告。
|
|
78
|
-
|
|
79
|
-
## 4. 排查(卡片不弹时按序查)
|
|
80
|
-
|
|
81
|
-
1. 开关是否真的生效:`GET /api/dsh-auto-memory-pre/auto-continue-state`(或设置页)看 `enabled`;
|
|
82
|
-
2. 日志有没有 `consolidate skip:` / `auto-continue` 任何一行;
|
|
83
|
-
3. `modelKnown` 是否为真:首轮必为假(设计如此),只在**轮末**才可能 arm;
|
|
84
|
-
4. 冷却:`autoContinueCooldownMinutes=30`,触发过一次后 30 分钟内不再弹——**验收期间只点一次**,要点第二次就先把它调小。
|
|
85
|
-
|
|
86
|
-
## 5. 回滚(验完必做)
|
|
87
|
-
|
|
88
|
-
```pwsh
|
|
89
|
-
Copy-Item "$env:USERPROFILE\.dsh\dsh-auto-memory-pre.json.bak-accept35" "$env:USERPROFILE\.dsh\dsh-auto-memory-pre.json" -Force
|
|
90
|
-
```
|
|
91
|
-
然后重启 dsh web。**除非用户明确要求长期开启**,否则恢复成 `handoffEnabled=false` / `autoContinueEnabled=false` / `autoContinueThreshold=0.75`。
|
|
92
|
-
|
|
93
|
-
> 待用户裁定(会写进交接账本):这两个开关**日常要不要开**。若长期关着,则 #35 与 #31 第二半这两笔修复在水位/接续/白板面上**处于不生效状态**——修了但不在线。
|
|
94
|
-
|
|
95
|
-
## 6. 纪律
|
|
96
|
-
|
|
97
|
-
- 改配置**必须先备份**(用户级硬规则);只改目标键,别整篇重写。
|
|
98
|
-
- **不要**按进程名杀进程(曾误杀 harness);要停就用精确 PID + `taskkill /PID <pid> /T /F`。
|
|
99
|
-
- 不要拿主对话当接续靶子。
|
|
100
|
-
- 验收结论无论通过与否都要落一笔到当日日志;失败要附原始日志片段,不要只写"没通过"。
|
|
101
|
-
|
|
102
|
-
## 7. 真机验收结果(2026-09-14 17:37–17:41,Run 1)
|
|
103
|
-
|
|
104
|
-
> 现场:旧会话 `session-d2c13583`(128 msgs)→ 新会话 `session-b8f093e4`。**在飞配置为 `handoffEnabled=true`**(非本文档 §2 设想的 A 轮),故本次实为 **B 轮**(完整两层载体);**A 轮(白板关)仍未验**。
|
|
105
|
-
> 验收人操作:用户在新窗口点「同意接续」(`decideAutoContinue('agree')`)。
|
|
106
|
-
|
|
107
|
-
### 判据结果
|
|
108
|
-
|
|
109
|
-
| # | 结果 | 证据 |
|
|
110
|
-
|---|---|---|
|
|
111
|
-
| 1 | **PASS** | `09:36:52.739Z auto-continue armed: sid=session-d2c13583… ratio=0.07 awaitIdle=true` |
|
|
112
|
-
| 2 | **PASS** | `09:37:37.776Z auto-continue: stopped old turn sid=session-d2c13583…`;旧会话 `turn/end reason.kind=aborted/user` |
|
|
113
|
-
| 3 | **FAIL(原因已查清,非逻辑缺陷)** | `09:39:08.082Z host refresh ritual: timeout, continuing with current material` → `lastOk.refreshRitual='timeout'` |
|
|
114
|
-
| 4 | **PASS** | `09:39:08.106Z prev-session pack built … contSeq=22` → `09:39:08.247Z auto-continue host-executed: new session session-b8f093e4… ritual=timeout stopped=ok` |
|
|
115
|
-
|
|
116
|
-
**反向断言**:④ 出现且 ② 同时出现 → 停旧回合生效;新会话成功建立(侧栏标题 `接续 #22 · dsh-auto-memory`,工作区/模型/权限 `deepseek-v4.1-flash` + `danger-full-access` 均继承)→ 无"比并行更糟"的交接失败。
|
|
117
|
-
|
|
118
|
-
### 判据 3 超时的根因(已用旧会话原始事件流取证)
|
|
119
|
-
|
|
120
|
-
不是判据写错,是**上游 API 故障把仪式推后到轮询窗之外**:
|
|
121
|
-
|
|
122
|
-
1. `09:37:37.778Z` 仪式以 `mode:'queue'` 投给旧会话,**立刻**落为 `seq=244 user/message`,`turn=3` 同步启动 → 投递链路本身正常。
|
|
123
|
-
2. `turn=3` 全窗没有一次成功的 assistant 产出:`llm/retry` 连续 5 次(`09:37:45`→`09:38:38`,61s),
|
|
124
|
-
`09:38:38.450Z turn/end reason.kind=error`,错误为 **`502: {"message":"上游拒绝请求","type":"upstream_bad_request"}`**。
|
|
125
|
-
3. 90s 轮询窗(`autoContinueRefreshTimeoutSeconds`)到点 → 报 `timeout`,宿主按 fail-soft 继续接续。
|
|
126
|
-
4. **仪式随后真的执行了**:用户后续「继续」推动 `turn=6`,`09:41:01.529Z`(seq 291–295)模型连调两次
|
|
127
|
-
`memory_note_pre`(`kind=plan` + `kind=handoff`)→ `PLAN.md` 与 `handoff-20260914-174101.md` 的 mtime 正是 `17:41:01`。
|
|
128
|
-
|
|
129
|
-
结论:**因果闭合判据的实质成立**(事件数增长且出现 seq>239 的 `assistant/message` + `tool/call`),只是发生在 90s 窗之后(+114s)。**记为半通过**,按本文档 §3 的口径单独记录;若要让它真通过,应提高 `autoContinueRefreshTimeoutSeconds`(当前上限 600)。
|
|
130
|
-
|
|
131
|
-
### 本轮新发现的真实风险:新会话会被立刻再次 arm(链式接续)
|
|
132
|
-
|
|
133
|
-
- `09:42:49.340Z auto-continue armed: sid=session-b8f093e4…`(**即刚建出来的新会话**)→ 若无人干预,约 35s 后会再次掐掉新会话并再建一个。
|
|
134
|
-
- 机制:`markContinuedSession(oldSid, newId)` 只给**旧**会话上闩(`isContinuedSession` 检查的是旧 sid);新会话是全新身份,不受闩保护。而新会话首题就是超大交接包,`ratio` 开局即 ≈0.053。
|
|
135
|
-
- 本次是阈值降到 `0.05` 的**验收态放大**了它;生产阈值 `0.75` 下新会话开局不可能越线,故**当前不判定为缺陷**,但值得在 3.1 决策:新会话是否也应有一段"免接续蜜月期"。
|
|
136
|
-
- **已处置**:`POST /auto-continue-decide {action:'reject'}` 已拆引信(`rejectedEdgeAt=1789378969340`),armed 清空。
|
|
137
|
-
|
|
138
|
-
### 收尾状态
|
|
139
|
-
|
|
140
|
-
- 配置已按 §5 回滚:`autoContinueEnabled=false` / `autoContinueThreshold=0.75` / `autoContinueCooldownMinutes=30`(`handoffEnabled` 保持 `true`,与备份一致)。sha `3DEE4041F3E9`。
|
|
141
|
-
- 回滚已**热生效**(`GET /api/dsh-auto-memory-pre/config` 内部会 `loadConfig()` 覆盖内存态;无需重启即生效,实测 `enabled:false`)——这与本文档 §2"每轮改完重启 dsh web"的旧说法不同,**热重载可用**。
|
|
142
|
-
- 验收态快照留存:`~/.dsh/dsh-auto-memory-pre.json.bak-accept35-run1-evidence`。
|
|
143
|
-
- **未验**:A 轮(`handoffEnabled=false`,判据 3′)、B 档"回合运行中点同意"(本次点同意时旧回合已自行 abort,`cancel` 对空转回合是空操作)。
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
# 验收报告 · ZCode 交班对账
|
|
2
|
-
|
|
3
|
-
> 验收时间:2026-09-14 02:2x–02:5x(本地)|验收方:DSH 主对话(**只做取证与复核,未改任何产品代码**)
|
|
4
|
-
> 验收对象:ZCode 交付的三任务 + 10 项追加需求 + 交班材料(对账单见对话与 `TODO-BACKLOG.md`)
|
|
5
|
-
> 口径:**能独立复核的写实测值;不能复核的明确标注"不可独立复核"**,不用转述充证据。
|
|
6
|
-
|
|
7
|
-
## 一、基线复核(实测)
|
|
8
|
-
|
|
9
|
-
| 项 | 报告值 | 实测(命令/位置) | 判定 |
|
|
10
|
-
| --- | --- | --- | --- |
|
|
11
|
-
| npm latest | 2.5.2 | `npm view @a9i5k4/dsh-auto-memory version --registry=https://registry.npmjs.org` → `2.5.2`;`dist-tags.latest=2.5.2` | ✅ |
|
|
12
|
-
| GitHub main = tag | `55c3deb` | REL 线 `D:\dsh_debug\_publish_dsh-auto-memory` HEAD = `55c3deb`,tag `v2.5.0/v2.5.1/v2.5.2` 齐 | ✅ |
|
|
13
|
-
| REL 工作树 | (未提) | 唯一未跟踪 = `.github/cloud/qq-webhook/index.zip`(发版纪律要求排除构建产物,正常) | ✅ |
|
|
14
|
-
| pre 线 HEAD | `747aa68` | **实测 `1c06c46`** —— 多出 4 个提交,全是 `docs/`:官方 discussion #5732 评论稿 3 版 + dshapi 中转反馈稿(**非 ZCode 线,是本机 DSH 会话产出**) | ⚠️ 偏差,已更正 |
|
|
15
|
-
| 冒烟套件 | 71 全绿 | 首次全量 **70/71**(`m73` 假红,见三)→ 夹具加固后 **71/71 全绿**,125s | ✅(修复后) |
|
|
16
|
-
| 静态检查 | — | `node --check lib/index.js`、`lib/client.js` 均 0;两文件首三字节非 `EF BB BF` | ✅ |
|
|
17
|
-
|
|
18
|
-
## 二、10 项追加需求逐项取证(全部落到源码/文件,可独立复核)
|
|
19
|
-
|
|
20
|
-
| # | 需求 | 取证点 | 判定 |
|
|
21
|
-
| --- | --- | --- | --- |
|
|
22
|
-
| 1 | PR #29 合并移植 | `lib/index.js:47` import `decodeZstdFramesHead`;调用点 `:1626`(巡检扫描)、`:1927`(`waterWindowForSession` 同防) | ✅ |
|
|
23
|
-
| 2 | procedure 链路审计 + 缺口记录 | 结论落在 `TODO-BACKLOG.md` §A 末条(数据层从未产出 active procedure、`renderChecklist` 无子代理出口、`memory_recall` 无 procedures scope)——**没有独立审计文件** | ⚠️ 审计在、未单独成文 |
|
|
24
|
-
| 3 | 白板整合预研 + 看板化评估 | `WB-GRAPH-RESEARCH-BRIEF.md`(7.7KB) / `-LOCAL.md`(21.2KB) / `-EXTERNAL.md`(33.2KB) / `WB-GRAPH-INTEGRATION-PLAN.md`(39.1KB),四份齐 | ✅ |
|
|
25
|
-
| 4 | 云函数定时自触发 | `.github/cloud/qq-webhook/index.js:366-368` timer 分支;`triggerName=digest_dispatch`;`TIMER_MIN_GAP_HOURS=10` 防重 | ✅ |
|
|
26
|
-
| 5 | @ 答疑限额 env 化 | `index.js:47-50` `AI_MAX_PER_HOUR`(0/缺省=不限)/`AI_QUOTA_HOURS`(默认 1h);配额落 gist | ✅ |
|
|
27
|
-
| 6 | 日报去重(prompt + 确定性双层) | `group-digest.mjs:91-93` 30 分钟 schedule 去重;`:400` prompt 规则「不得新增重复条目」;`:417` 解析失败兜底按标题前 8 字去重 | ✅ |
|
|
28
|
-
| 7 | 未解决事项跨期跟踪 + 自动销账 | `group-digest.mjs:327-331`(open/resolved/stale,14 天归档)、`:400` `resolved_titles` 规则、`:234` 日报渲染「✅ 已解决并移出」 | ✅ |
|
|
29
|
-
| 8 | 反馈文件钉死 | 提交 `9f594db`「反馈文件钉死文件名(修复第一个文件换人/清空即删文件)」,code 侧仍在 | ✅ |
|
|
30
|
-
| 9 | 思维链截断修复 | `index.js:232-249` `max_tokens: 1600` + 三层过滤(`reasoning_content` → `</think>` 切段 → 行首思考特征词剥除) | ✅ |
|
|
31
|
-
| 10 | mention id 变更失聪修复 | `index.js:308-310` 学习改为**取最新见到的 mention** 为准;`:479` diag 暴露 `mentionLearned` | ✅ |
|
|
32
|
-
|
|
33
|
-
另:源码 `VERSION = 'webhook-gist-20260913n'`(`index.js:56`)——与"线上 n 版"的自述一致。
|
|
34
|
-
|
|
35
|
-
## 三、本轮发现并修复的问题(1 项真问题 + 1 条教训)
|
|
36
|
-
|
|
37
|
-
**① `smoke-test-m73-pre.mjs` 负载敏感假红(真问题,已修)**
|
|
38
|
-
|
|
39
|
-
- 现象:全量连跑 FAIL;单跑第 1 次也 FAIL,第 2 次绿 → 非确定。失败点在 N8「模型缺失 → enabled/ready」:`h.frame.payload` 为 undefined 抛 `TypeError`。
|
|
40
|
-
- 根因:夹具 `requestTimeoutMs: 8000`,连跑时 python worker 冷启动超过窗口 → `health()` 返回**无 frame** 的失败响应(属夹具鲁棒性,不是产品缺陷)。
|
|
41
|
-
- 修法:新增 `healthReady(c, attempts=8, gapMs=400)` 有界轮询,**只对"无 frame"重试**;9 处 `c.health()` 全部改走它。断言(`embedding.enabled/ready/error`)一字未改 —— worker 真起不来时轮询耗尽、拿最后一个无 frame 响应、断言照常失败,**不掩蔽真故障**。
|
|
42
|
-
- 验证:单跑 3/3 绿(`pass=59 fail=0`);全量 71/71 绿。
|
|
43
|
-
|
|
44
|
-
**② 教训(写进本项目纪律)**:`replace_all` 会命中**同一次编辑新加入的代码块** —— 本次把辅助函数内部的 `c.health()` 也替换成了 `healthReady(c)`,造成自递归、3 连红。替换后必须核对命中计数(预期 9 实得 10 即报警)并 `node --check`。
|
|
45
|
-
|
|
46
|
-
## 四、不可独立复核项(如实标注,不作为验收结论)
|
|
47
|
-
|
|
48
|
-
- **线上云函数部署版本**:函数 URL 与 `ROUTE_TOKEN` 不在仓库(符合凭据纪律),只能由用户/群内实测确认;本报告只证明**源码 = n 版**。
|
|
49
|
-
- **群内实战验证("没毛病了")**:外部证据,仓库侧无法复核。
|
|
50
|
-
- **两个外部时间点**:今早 11:40 定时触发器首跑、下期日报应把「工作区切换问题」按 v2.5.2 自动销账 —— 时间未到(现 2026-09-14 02:5x),不可验。
|
|
51
|
-
|
|
52
|
-
## 五、结论
|
|
53
|
-
|
|
54
|
-
1. **ZCode 三问三答的 ①发版 ②`?report=N` 排查 ③群反馈调研**:仓库侧证据充分,成立;本机 DSH 侧对 71 套件的独立复跑同样通过(修复夹具后)。
|
|
55
|
-
2. **10 项追加需求**:9 项源码取证完全成立;第 2 项(procedure 审计)结论存在但未单独成文。
|
|
56
|
-
3. **报告偏差 2 处**(均已修正到文档):
|
|
57
|
-
- `pre` 线 HEAD 不是 `747aa68` 而是 `1c06c46`(+4 个 docs 提交,与本机 DSH 会话的 harness bug 上报有关)。
|
|
58
|
-
- 白板 `PLAN.md` 当时写着"下一版深改三点仍未动",**与事实相反**(水位口径 / 接续序号 / 流程外包三项已于 v2.5.0 落地并发布)——白板已重写。
|
|
59
|
-
4. **真正剩余的待办**只分三类:①**阻塞在用户的决策**(大排期 6 拍板点 + 3 样输入;procedure 机制整体重构)②**待外部时间点**(定时班首跑、下期日报销账)③**可排期的存量项**(`TODO-BACKLOG` §C 冷启动 5 项 / §D 分发门面 / §E 文档体系 —— 已复核仍成立,但非当前焦点)。
|
|
60
|
-
|
|
61
|
-
---
|
|
62
|
-
|
|
63
|
-
### 附:本机"会话语义检索"不可用的根因与处置(2026-09-14 追查,已实施待重启生效)
|
|
64
|
-
|
|
65
|
-
**现象**:`memory_recall(scope='sessions')` 报 `session search is disabled: this deployment configures the session-query index with openAt "never"`。
|
|
66
|
-
|
|
67
|
-
**根因(逐层取证;不是"没装",是"从没打开")**:
|
|
68
|
-
|
|
69
|
-
1. 抛错点在 `@deepseek-ai/dsh-session-query-sqlite/lib/index.js:594-595`:`openAt === 'never'` 时在任何 SQLite 动作之前直接抛 `SESSION_QUERY_SEARCH_DISABLED`。
|
|
70
|
-
2. `never` 是**随包默认**,写在 `@deepseek-ai/dsh-base/cordis.patch.yml:121-133`(该行注释原文:「Full-text session search is **opt-in**. `openAt: never` …」),配置为 `path: ':memory:'` + `openAt: never`。
|
|
71
|
-
3. 同一段注释**给出了开启方式**:「Deployments enabling content search override `openAt` to `first-search` or `startup` in a later patch layer (**profile cordis.patch.yml** or a `--patch` overlay), typically with a durable `path`。」
|
|
72
|
-
4. 本机用户层 `~/.dsh/profiles/web/cordis.patch.yml`(最后应用)原只有 3 条 `disabled: true`,**从未覆盖过这一行** → 检索一直是关的。**更正**:先前报告写「值来自 launcher 默认、属 DSH 侧改动」不准确 —— 值是 dsh-base bundle 的随包默认,而覆盖层正是**用户可改**的 profile patch。
|
|
73
|
-
5. 插件侧无额外开关:`lib/index.js:1804-1807` 直接调 `sq.searchSessions(...)`,没有自己的门闸 —— 唯一阻塞就是上面这一行。
|
|
74
|
-
|
|
75
|
-
**处置(已落盘)**:在 `~/.dsh/profiles/web/cordis.patch.yml` 追加 id 定向覆盖(原件已备份为同目录 `cordis.patch.yml.bak-20260914-search`):
|
|
76
|
-
|
|
77
|
-
```yaml
|
|
78
|
-
- id: session-query-sqlite
|
|
79
|
-
config:
|
|
80
|
-
path: 'C:/Users/JH Z/.dsh/profiles/web/session-query.db'
|
|
81
|
-
openAt: first-search
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
关键约束:patch 对目标行是**整块 `config` 替换而非合并** → `path` 必须一并重述(`path` 是 `z.string().required()`,省略即配置非法);`path` 取 **profile 私有**路径,避免与其它 profile/进程争用同一索引文件(该索引每路径单进程持有)。
|
|
85
|
-
|
|
86
|
-
**校验证据(不启动宿主,`dsh --profile web --dump-config`)**:退出码 0;合成结果该行 `name` 仍由 base 层给出、仅 `config` 被替换;全树 `openAt` **只出现 1 次** = `first-search`;`- id: session-query-sqlite` 恰 1 条(无重复行);后端为 **`node:sqlite`(Node v24.18.0 内置,`require('node:sqlite')` 实测可用,不需原生模块)**;目标目录尚无 db 文件(首次检索时创建,属预期);文件首 3 字节非 `EF BB BF`。
|
|
87
|
-
|
|
88
|
-
**生效条件**:宿主只在启动时读该文件 → **需用户自行重启 dsh web**;重启后首次检索即建索引(`first-search`)。副作用:Web 侧边栏搜索从"只匹配标题/工作区名"升级为内容检索。
|
|
89
|
-
|
|
90
|
-
**与它无关但一直可用**:插件的跨工作区记忆检索(日志/笔记/反思)与 `handoff/` 分层转写材料 —— 本次验收全程用的是后者。
|