@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.
Files changed (68) hide show
  1. package/README.md +20 -10
  2. package/README.zh-CN.md +22 -10
  3. package/docs/CONTINUITY-FLOW.md +222 -0
  4. package/docs/HANDBOOK.md +354 -0
  5. package/docs/INTEGRATION-ANALYSIS.md +348 -0
  6. package/docs/M-CM7-HANDOFF-LAYERED-RETRIEVAL.md +311 -0
  7. package/docs/M8-MEMORY-HUB.md +1 -1
  8. package/docs/PROMPT-PACK-LAYERED-RECALL.md +474 -0
  9. package/docs/PROMPT-SET-STRICT.md +389 -0
  10. package/docs/RELEASE-GO-NOGO.md +82 -0
  11. package/docs/ROADMAP.md +162 -0
  12. package/docs/STATUS-BOARD.md +147 -0
  13. package/docs/USER-GUIDE.en.md +382 -0
  14. package/docs/USER-GUIDE.zh-CN.md +382 -289
  15. package/docs/prompts/EXEC-ORDER.md +77 -0
  16. package/docs/prompts/FEEDING-SCRIPT.md +174 -0
  17. package/docs/prompts/FEEDING-SEQUENCE.md +61 -0
  18. package/docs/prompts/FIX-AGENT-M8-2b.md +119 -0
  19. package/docs/prompts/FIX-AGENT-P11.md +97 -0
  20. package/docs/prompts/FIX-AGENT-P12-FULL-REGRESSION.md +135 -0
  21. package/docs/prompts/FIX-AGENT-P12.md +113 -0
  22. package/docs/prompts/FIX-AGENT-P13-PYTHON-RANK.md +100 -0
  23. package/docs/prompts/FIX-AGENT-P8.md +120 -0
  24. package/docs/prompts/FIX-AGENT-P9.md +110 -0
  25. package/docs/prompts/FIX-AGENT-P9a.md +94 -0
  26. package/docs/prompts/FIX-AGENT-P9d.md +114 -0
  27. package/docs/prompts/FIX-AGENT-TEMPORAL-ARM.md +148 -0
  28. package/docs/prompts/LIVE-VERIFY-ZCODE.md +105 -0
  29. package/docs/prompts/M8-1-fact-metadata.md +45 -0
  30. package/docs/prompts/M8-2-ADJUDICATION.md +98 -0
  31. package/docs/prompts/M8-2-importance-wiring.md +42 -0
  32. package/docs/prompts/M8-2b-evidence-pipeline.md +52 -0
  33. package/docs/prompts/M8-3-enable-verify.md +49 -0
  34. package/docs/prompts/M8-R-REPORT.md +156 -0
  35. package/docs/prompts/M8-R-research.md +67 -0
  36. package/docs/prompts/P1-l0-index.md +30 -0
  37. package/docs/prompts/P10-importance-calibration.md +45 -0
  38. package/docs/prompts/P11-silent-catch-observability.md +43 -0
  39. package/docs/prompts/P2-semantic-recall.md +30 -0
  40. package/docs/prompts/P3-fusion.md +28 -0
  41. package/docs/prompts/P4-l0-response.md +28 -0
  42. package/docs/prompts/P5-handoff-anchor.md +28 -0
  43. package/docs/prompts/P6-ledger-weight.md +27 -0
  44. package/docs/prompts/P7-write-fix.md +26 -0
  45. package/docs/prompts/P8-rrf-wiring.md +47 -0
  46. package/docs/prompts/P9-REVIEW-DECISION.md +95 -0
  47. package/docs/prompts/P9-evidence-write-coverage.md +113 -0
  48. package/docs/prompts/README.md +105 -0
  49. package/docs/prompts/ZCODE-DROPIN.md +229 -0
  50. package/docs/prompts/_COMMON.md +88 -0
  51. package/lib/client.js +36 -2
  52. package/lib/context-host.js +77 -2
  53. package/lib/evidence-agg.js +81 -0
  54. package/lib/fact-store.js +32 -0
  55. package/lib/handoff-anchor.js +114 -0
  56. package/lib/index.js +402 -43
  57. package/lib/l0-extract.js +149 -0
  58. package/lib/l0-index.js +239 -0
  59. package/lib/m7-wire.js +4 -3
  60. package/lib/memory-importance.js +70 -0
  61. package/lib/python-setup.js +16 -4
  62. package/lib/recall-fusion.js +99 -0
  63. package/lib/shadow-retrieval.js +2 -2
  64. package/lib/storage-manage.js +17 -0
  65. package/lib/subagent-gc.js +8 -1
  66. package/lib/temporal-parse.js +159 -0
  67. package/package.json +1 -1
  68. package/python/worker_semantic_v1.py +28 -1
@@ -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,
@@ -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
- await rename(c.dir, destDir)
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.2.6",
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
- if not isinstance(obj, dict) or not base.envelope_shape_ok(obj):
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()