@jjh32456/dsh-longlive 1.6.7 → 1.8.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 (289) hide show
  1. package/CHANGELOG.md +70 -1508
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +34 -13
  4. package/README.en.md +18 -7
  5. package/README.md +34 -9
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +23 -9
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +4 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +168 -35
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +96 -28
  16. package/docs/MIGRATION-1.6.0.md +2 -2
  17. package/docs/OPERATIONS.md +6 -6
  18. package/docs/PANEL-INTEGRATION.md +6 -0
  19. package/docs/PRIVACY.md +88 -0
  20. package/docs/QUICKSTART.md +5 -5
  21. package/docs/README.en.md +4 -2
  22. package/docs/RESIDENT-WATCHER.md +1 -1
  23. package/docs/SESSION-WATCH.md +6 -6
  24. package/docs/TROUBLESHOOTING.md +4 -4
  25. package/package.json +47 -117
  26. package/runbook.md +2 -2
  27. package/scripts/backfill-auto-cards.mjs +98 -0
  28. package/scripts/install-surgery-task.ps1 +5 -2
  29. package/scripts/lib/session-watch-fixture.mjs +1 -1
  30. package/scripts/longlive-doctor.mjs +6 -0
  31. package/scripts/migrate-180.mjs +163 -0
  32. package/scripts/recall-index-build.mjs +128 -0
  33. package/scripts/session-watch-drill.mjs +1 -1
  34. package/scripts/surgery-run.cmd +1 -1
  35. package/scripts/unattended-surgery.ps1 +5 -2
  36. package/scripts/verify-card-anchors.mjs +138 -0
  37. package/src/actions/audit.js +125 -0
  38. package/src/actions/evidence.js +122 -0
  39. package/src/actions/fs.js +162 -0
  40. package/src/activity/index.js +760 -0
  41. package/src/activity/model.js +351 -0
  42. package/src/activity/store.js +644 -0
  43. package/src/activity/thinking.js +176 -0
  44. package/src/adapters/constants.js +11 -0
  45. package/src/adapters/engram/data/aliases.json +51 -0
  46. package/src/adapters/engram/data/policy.json +8 -0
  47. package/src/adapters/engram/data/routing-rules.json +546 -0
  48. package/src/adapters/engram/index.js +235 -0
  49. package/src/adapters/registry.js +315 -0
  50. package/src/answer/audit.js +193 -0
  51. package/src/answer/evidence.js +30 -0
  52. package/src/answer/guard.js +1370 -0
  53. package/src/answer/index.js +1067 -0
  54. package/src/answer/query.js +111 -0
  55. package/src/answer/second-hop.mjs +101 -0
  56. package/src/auto-truncate.js +1 -1
  57. package/src/config.js +171 -47
  58. package/src/deps.js +6 -4
  59. package/src/features.js +182 -6
  60. package/src/heartbeat.js +156 -23
  61. package/src/identity/handover.js +52 -1
  62. package/src/identity/index.js +13 -5
  63. package/src/identity/inject-position.js +2 -2
  64. package/src/index.js +97 -7
  65. package/src/lifecycle/gap-clocks.js +328 -0
  66. package/src/lifecycle/index.js +269 -0
  67. package/src/live.js +22 -2
  68. package/src/memory/activity-capture.js +229 -0
  69. package/src/memory/anchors.js +194 -0
  70. package/src/memory/audit-records.js +222 -0
  71. package/src/memory/auto-cards.js +401 -0
  72. package/src/memory/capture.js +442 -0
  73. package/src/memory/card-anchors.js +203 -0
  74. package/src/memory/conflicts.js +106 -0
  75. package/src/memory/decay.js +311 -0
  76. package/src/memory/distill.js +471 -0
  77. package/src/memory/event-index.js +917 -0
  78. package/src/memory/evidence-chain.js +295 -0
  79. package/src/memory/evidence.js +145 -0
  80. package/src/memory/filter-runner.js +1 -1
  81. package/src/memory/filter.js +5 -1
  82. package/src/memory/forget-v2.js +410 -0
  83. package/src/memory/forget.js +71 -0
  84. package/src/memory/frames-activation.js +247 -0
  85. package/src/memory/fts-query.js +174 -0
  86. package/src/memory/governance.js +604 -0
  87. package/src/memory/index.js +1 -0
  88. package/src/memory/l2-gate.js +169 -0
  89. package/src/memory/lock.js +121 -0
  90. package/src/memory/migrate.js +139 -0
  91. package/src/memory/recall-v2.js +223 -0
  92. package/src/memory/recall.js +65 -2
  93. package/src/memory/records.js +425 -0
  94. package/src/memory/retention.js +128 -0
  95. package/src/memory/review.js +124 -0
  96. package/src/memory/trust.js +88 -0
  97. package/src/memory/version-chain.js +264 -0
  98. package/src/memory/wal.js +137 -0
  99. package/src/memory/working-frame.js +516 -0
  100. package/src/metrics/classify.mjs +5 -3
  101. package/src/metrics/index.mjs +21 -1
  102. package/src/metrics/prestep-latency.mjs +300 -0
  103. package/src/metrics/store.mjs +25 -0
  104. package/src/policy-profile.js +18 -3
  105. package/src/recall/anaphora.mjs +87 -0
  106. package/src/recall/cards.mjs +7 -0
  107. package/src/recall/embedding.mjs +8 -8
  108. package/src/recall/entities.mjs +112 -0
  109. package/src/recall/index.js +235 -19
  110. package/src/recall/inject-order.mjs +10 -8
  111. package/src/recall/privacy.mjs +105 -16
  112. package/src/recall/recall-core.mjs +116 -10
  113. package/src/recall/shared-visibility.js +302 -0
  114. package/src/recall/time-normalizer.mjs +196 -0
  115. package/src/role/activation.js +191 -0
  116. package/src/role/attention.js +318 -0
  117. package/src/role/base.js +36 -0
  118. package/src/role/channels.js +423 -0
  119. package/src/role/index.js +425 -0
  120. package/src/role/interfaces.js +115 -0
  121. package/src/role/personality.js +370 -0
  122. package/src/role/readers.js +151 -0
  123. package/src/role/relationships.js +477 -0
  124. package/src/role/store.js +202 -0
  125. package/src/role/writeprotect.js +373 -0
  126. package/src/router/aliases.json +176 -25
  127. package/src/router/background/agent-runner.mjs +1 -1
  128. package/src/router/background/anchor.mjs +1 -1
  129. package/src/router/background/audit.mjs +2 -2
  130. package/src/router/background/budget-ledger.mjs +2 -2
  131. package/src/router/background/host-sim.mjs +1 -1
  132. package/src/router/background/memory.mjs +4 -4
  133. package/src/router/background/queue.mjs +1 -1
  134. package/src/router/background/retry.mjs +1 -1
  135. package/src/router/background/router.mjs +1 -1
  136. package/src/router/background/stream.mjs +1 -1
  137. package/src/router/background/tool-policy.mjs +1 -1
  138. package/src/router/design-headings.json +5 -5
  139. package/src/router/face/config.mjs +1 -1
  140. package/src/router/face/identity.mjs +3 -3
  141. package/src/router/judge.mjs +8 -7
  142. package/src/router/policies/generic.json +21 -4
  143. package/src/router/route.js +109 -22
  144. package/src/router/router.mjs +10 -6
  145. package/src/router/routing-rules.json +260 -445
  146. package/src/router/run-sim.mjs +8 -8
  147. package/src/router/runtime.js +23 -12
  148. package/src/router/service/board.mjs +95 -15
  149. package/src/router/service/config.mjs +5 -5
  150. package/src/router/service/context.mjs +1 -1
  151. package/src/router/service/hint.mjs +14 -0
  152. package/src/router/service/queue.mjs +1 -1
  153. package/src/router/service/service.mjs +27 -11
  154. package/src/router/shadow.mjs +4 -3
  155. package/src/surgery-recovery.js +2 -2
  156. package/src/tool-output.js +2 -2
  157. package/src/tools.js +39 -7
  158. package/src/truncate-visible.js +2 -2
  159. package/src/ui/actions.js +1312 -0
  160. package/src/ui/panel-api.js +367 -0
  161. package/src/ui/panel-auth.js +251 -0
  162. package/src/ui/panel-integrity.js +243 -0
  163. package/src/ui/routes-readonly.js +695 -0
  164. package/src/ui/routes.js +3 -0
  165. package/docs/DEV.md +0 -279
  166. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  167. package/docs/PRESENCE-BUS.md +0 -694
  168. package/docs/SURGERY.md +0 -186
  169. package/scripts/mark-core-cards.mjs +0 -289
  170. package/scripts/pack.mjs +0 -392
  171. package/scripts/router-proposals.mjs +0 -386
  172. package/src/router/fixtures/M1.json +0 -16
  173. package/src/router/fixtures/M2.json +0 -16
  174. package/src/router/fixtures/M3.json +0 -20
  175. package/src/router/fixtures/M4.json +0 -20
  176. package/src/router/fixtures/M5.json +0 -45
  177. package/src/router/fixtures/M6.json +0 -53
  178. package/src/router/fixtures/M7.json +0 -46
  179. package/src/router/fixtures/S1.json +0 -20
  180. package/src/router/fixtures/S2.json +0 -23
  181. package/src/router/fixtures/S3.json +0 -20
  182. package/src/router/fixtures/S4.json +0 -34
  183. package/src/router/fixtures/S5.json +0 -43
  184. package/src/router/fixtures/S6.json +0 -47
  185. package/src/router/fixtures/T1.json +0 -23
  186. package/src/router/fixtures/T2.json +0 -16
  187. package/src/router/fixtures/T3.json +0 -19
  188. package/src/router/fixtures/T4.json +0 -16
  189. package/src/router/fixtures/X1.json +0 -17
  190. package/src/router/fixtures/X2.json +0 -20
  191. package/src/router/gen-design-headings.mjs +0 -107
  192. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  193. package/test/auto-recall.test.mjs +0 -474
  194. package/test/auto-truncate.test.mjs +0 -931
  195. package/test/autobind.test.mjs +0 -385
  196. package/test/bus.test.mjs +0 -1572
  197. package/test/cli-guard.test.mjs +0 -198
  198. package/test/conclusion.test.mjs +0 -408
  199. package/test/config-schema.test.mjs +0 -204
  200. package/test/d6-output.test.mjs +0 -194
  201. package/test/d6.test.mjs +0 -395
  202. package/test/deps.test.mjs +0 -342
  203. package/test/doctor-setup.test.mjs +0 -297
  204. package/test/embedding-recall.test.mjs +0 -347
  205. package/test/feature-flags.test.mjs +0 -568
  206. package/test/filter-shadow.test.mjs +0 -170
  207. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  208. package/test/fixtures/d6-header-tools.json +0 -1
  209. package/test/fixtures/lock-child.mjs +0 -67
  210. package/test/fixtures/pre-step-container.mjs +0 -100
  211. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  212. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  213. package/test/fixtures/profile-session-longlive-config.json +0 -20
  214. package/test/fixtures/proposal-cas-child.mjs +0 -66
  215. package/test/host-engine-status.test.mjs +0 -307
  216. package/test/hot-truncate.test.mjs +0 -1349
  217. package/test/identity-card-marker.test.mjs +0 -307
  218. package/test/identity-router/run.mjs +0 -721
  219. package/test/integration-pre-step.test.mjs +0 -553
  220. package/test/live.test.mjs +0 -1020
  221. package/test/maintenance-schedule.test.mjs +0 -538
  222. package/test/memory.test.mjs +0 -2828
  223. package/test/metrics.test.mjs +0 -478
  224. package/test/orchestrator.test.mjs +0 -266
  225. package/test/p1_probe.mjs +0 -97
  226. package/test/packaging.test.mjs +0 -182
  227. package/test/portability.test.mjs +0 -343
  228. package/test/rc4-regressions.test.mjs +0 -562
  229. package/test/rc5-regressions.test.mjs +0 -197
  230. package/test/rc6-chinese.test.mjs +0 -394
  231. package/test/router-1100/apply-off.test.mjs +0 -175
  232. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  233. package/test/router-1100/config-context.test.mjs +0 -211
  234. package/test/router-1100/helpers.mjs +0 -129
  235. package/test/router-1100/hint-envelope.test.mjs +0 -150
  236. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  237. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  238. package/test/router-1100/router-apply.test.mjs +0 -372
  239. package/test/router-1100/router-fixb.test.mjs +0 -602
  240. package/test/router-1100/router-proposals.test.mjs +0 -405
  241. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  242. package/test/router-1100/router-security-cr.test.mjs +0 -333
  243. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  244. package/test/router-1100/tools-contract.test.mjs +0 -164
  245. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  246. package/test/router-1100/v6-contract.test.mjs +0 -158
  247. package/test/router-1100/v6b-contract.test.mjs +0 -293
  248. package/test/router-1100/v6c-contract.test.mjs +0 -249
  249. package/test/router-1100/wal-rotation.test.mjs +0 -375
  250. package/test/router-1100/wp1-config.test.mjs +0 -170
  251. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  252. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  253. package/test/router-1100/wp1-status.test.mjs +0 -220
  254. package/test/router-1100/wp1-tools.test.mjs +0 -146
  255. package/test/router-1100/wp5-cli.test.mjs +0 -240
  256. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  257. package/test/router-1100/wp6-merge.test.mjs +0 -386
  258. package/test/router-1100/wp6-wake.test.mjs +0 -502
  259. package/test/router-background/audit.test.mjs +0 -37
  260. package/test/router-background/budget-ledger.test.mjs +0 -330
  261. package/test/router-background/cross-process.test.mjs +0 -222
  262. package/test/router-background/fixtures/cross-child.mjs +0 -85
  263. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  264. package/test/router-background/router.test.mjs +0 -1044
  265. package/test/router-background/stream-retry.test.mjs +0 -358
  266. package/test/router-background/tool-agent.test.mjs +0 -384
  267. package/test/router-face/bridge.test.mjs +0 -169
  268. package/test/router-face/face.test.mjs +0 -316
  269. package/test/router-failclosed.test.mjs +0 -466
  270. package/test/router-identity.test.mjs +0 -384
  271. package/test/router-l1-l2.contract.test.mjs +0 -1075
  272. package/test/router-memory-tools.test.mjs +0 -262
  273. package/test/router-policy.test.mjs +0 -225
  274. package/test/router-runtime.test.mjs +0 -438
  275. package/test/router-shadow.test.mjs +0 -287
  276. package/test/router.test.mjs +0 -903
  277. package/test/run-rc.mjs +0 -299
  278. package/test/run.mjs +0 -2469
  279. package/test/safety-defaults.test.mjs +0 -179
  280. package/test/seeded-load.test.mjs +0 -436
  281. package/test/session-original.test.mjs +0 -338
  282. package/test/surgery-recovery.test.mjs +0 -936
  283. package/test/t7_drive.mjs +0 -151
  284. package/test/tool-output.test.mjs +0 -393
  285. package/test/tool-schema.test.mjs +0 -177
  286. package/test/tools_contract.test.mjs +0 -716
  287. package/test/truncate-visible.test.mjs +0 -1150
  288. package/test/ui-flags.test.mjs +0 -477
  289. package/test/v4-hardening.test.mjs +0 -427
@@ -0,0 +1,247 @@
1
+ // src/memory/frames-activation.js — 工作帧运行期激活探针(任务「功能没跑起来就等于没实现」)
2
+ //
3
+ // 与 `src/role/activation.js#probeRoleActivation` 对齐:不是单测替身,而是按宿主真实装配
4
+ // 路径跑一遍 ——
5
+ // 1) `registerFeatures(ctx, config{memoryV2.enabled:true})` 真实注册 working-frame 的
6
+ // `agent/pre-step` hook(不是直接手动 createWorkingFrameStore);
7
+ // 2) 用真实 `(payload, next)` 瀑布形态驱动该 hook 两轮:第一轮建帧、第二轮注入;
8
+ // 3) 注入走真实 `composePreStepBlocks` + `insertWorkingMessage`([boot, shared, working, recall]
9
+ // 顺序里 working 在 recall 之前),帧更新走真实 `updateFrameFromMessages → updateFocus`;
10
+ // 4) 再回读落盘帧 + HMAC 侧车校验 + 篡改 fail-safe。
11
+ //
12
+ // 供 `scripts/verify-activation.mjs`(acceptance-harness 的激活门禁 A8)调用:
13
+ // 入口:`src/memory/frames-activation.js` 导出 `probeFrameActivation`(供 verify-activation.mjs 调用)
14
+ // const frames = probeFrameActivation({ memoryRoot }); // ok:true 才算激活
15
+ // const killed = await probeFrameActivation({ killSwitch: true }); // enabled:false + 零文件
16
+ //
17
+ // kill switch 语义与 features 一致:`memoryV2.enabled:false`(帧只随 memoryV2/workingFrame 开启)
18
+ // 或 `options.workingFrame:false` / env `DSH_LONGLIVE_DISABLE_MEMORY_V2=1`。纯本地、不联网。
19
+
20
+ import fs from 'node:fs';
21
+ import os from 'node:os';
22
+ import path from 'node:path';
23
+
24
+ import { registerFeatures } from '../features.js';
25
+ import { createWorkingFrameStore } from './working-frame.js';
26
+ import { AUTO_TEXT_MARK } from '../identity/inject-position.js';
27
+ import { SIDECAR_SUFFIX, verifyProtectedFile } from '../role/writeprotect.js';
28
+
29
+ export const FRAME_ACTIVATION_SCHEMA = 'frame-activation/v1';
30
+ export const FRAME_ACTIVATION_CHECKS = Object.freeze([
31
+ 'enabled',
32
+ 'preStepRegistered',
33
+ 'turnProcessed',
34
+ 'frameFilePresent',
35
+ 'hmacSidecarValid',
36
+ 'frameLoaded',
37
+ 'eNoFrameAbsentAfterTurn',
38
+ 'injectionTextInjected',
39
+ 'injectionBeforeRecall',
40
+ 'focusUpdated',
41
+ 'tamperFailSafe',
42
+ 'restoredInjecting',
43
+ ]);
44
+
45
+ function humanMessage(text) {
46
+ return {
47
+ role: 'user',
48
+ source: { kind: 'user' },
49
+ content: [{ type: 'text', text }],
50
+ };
51
+ }
52
+
53
+ function messageText(message) {
54
+ return (Array.isArray(message?.content) ? message.content : [])
55
+ .filter((block) => block?.type === 'text' && typeof block.text === 'string')
56
+ .map((block) => block.text)
57
+ .join('\n');
58
+ }
59
+
60
+ function recallMessage(text) {
61
+ return {
62
+ role: 'user',
63
+ source: { kind: 'auto' },
64
+ content: [{ type: 'text', text: `${AUTO_TEXT_MARK} ${text}` }],
65
+ };
66
+ }
67
+
68
+ /**
69
+ * Run the real host-shaped frames activation probe.
70
+ *
71
+ * @param {{memoryRoot?:string, now?:()=>Date, monoNow?:()=>number, killSwitch?:boolean, keep?:boolean}} [options]
72
+ * @returns {Promise<object>} {ok, schema, enabled, checks[], counters, injectedText, frame, files}
73
+ */
74
+ export async function probeFrameActivation(options = {}) {
75
+ const ownsRoot = typeof options.memoryRoot !== 'string' || options.memoryRoot.trim() === '';
76
+ const memoryRoot = ownsRoot
77
+ ? fs.mkdtempSync(path.join(os.tmpdir(), 'longlive-frames-activation-'))
78
+ : path.resolve(options.memoryRoot.trim());
79
+ const keep = options.keep === true || !ownsRoot;
80
+ const now = typeof options.now === 'function' ? options.now : () => new Date('2026-11-07T04:00:00.000Z');
81
+ let mono = 1000;
82
+ const monoNow = typeof options.monoNow === 'function'
83
+ ? options.monoNow
84
+ : () => { mono += 10; return mono; };
85
+ const checks = [];
86
+ const push = (name, ok, detail = null) => checks.push({ name, ok: ok === true, detail });
87
+ const counters = {
88
+ preStepCalls: 0,
89
+ injections: 0,
90
+ hookCount: 0,
91
+ tamperRejections: 0,
92
+ updateFocusRuns: 0,
93
+ };
94
+
95
+ try {
96
+ fs.mkdirSync(memoryRoot, { recursive: true });
97
+ // 真实宿主装配:只开 memoryV2(working-frame 随它),role 显式 kill 以隔离证据。
98
+ const enabled = options.killSwitch !== true;
99
+ const config = {
100
+ memoryV2: { enabled },
101
+ role: { enabled: false },
102
+ };
103
+ const preStepHandlers = [];
104
+ const warnings = [];
105
+ const ctx = {
106
+ on(name, handler) {
107
+ if (name === 'agent/pre-step') preStepHandlers.push(handler);
108
+ return () => {};
109
+ },
110
+ logger: () => ({
111
+ warn: (message) => warnings.push(String(message)),
112
+ info() {},
113
+ error() {},
114
+ debug() {},
115
+ }),
116
+ effect(fn) {
117
+ try {
118
+ const dispose = fn();
119
+ return typeof dispose === 'function' ? dispose : () => {};
120
+ } catch {
121
+ return () => {};
122
+ }
123
+ },
124
+ tools: { register() { return () => {}; } },
125
+ };
126
+ const registered = registerFeatures(ctx, config, {
127
+ memoryRoot,
128
+ workingFrame: true,
129
+ now,
130
+ monoNow,
131
+ });
132
+ counters.hookCount = preStepHandlers.length;
133
+ push('enabled', enabled === true, enabled ? 'memoryV2 on' : 'kill switch (memoryV2.enabled=false)');
134
+ push('preStepRegistered', enabled === false
135
+ ? preStepHandlers.length === 0
136
+ : preStepHandlers.length === 1, preStepHandlers.length);
137
+
138
+ if (enabled === false) {
139
+ // kill 反例:无 pre-step hook、零 working 文件(帧不落盘)。
140
+ const workingDir = path.join(memoryRoot, '.longlive', 'working');
141
+ const files = fs.existsSync(workingDir) ? fs.readdirSync(workingDir) : [];
142
+ push('turnProcessed', true);
143
+ push('frameFilePresent', files.length === 0, files);
144
+ push('hmacSidecarValid', true);
145
+ push('frameLoaded', true);
146
+ push('eNoFrameAbsentAfterTurn', true);
147
+ push('injectionTextInjected', true);
148
+ push('injectionBeforeRecall', true);
149
+ push('focusUpdated', true);
150
+ push('tamperFailSafe', true);
151
+ push('restoredInjecting', true);
152
+ return {
153
+ schema: FRAME_ACTIVATION_SCHEMA,
154
+ ok: false,
155
+ enabled: false,
156
+ killSwitch: true,
157
+ checks,
158
+ counters,
159
+ injectedText: '',
160
+ frame: null,
161
+ hooks: [],
162
+ registered: [],
163
+ frame: null,
164
+ files: { workingDir, files },
165
+ memoryRoot,
166
+ warnings,
167
+ };
168
+ }
169
+
170
+ const sessionId = 'sess-frames-activation-1';
171
+ const agent = { id: sessionId, session: { id: sessionId, header: { id: sessionId, origin: 'main' } } };
172
+ const store = createWorkingFrameStore({ memoryRoot, now, monoNow });
173
+ // 真实 pre-step 瀑布:step=1、非 subagent、decision 带 messages。
174
+ const invokeTurn = async (turn, humanText) => {
175
+ const payload = { agent, step: 1, turn };
176
+ const human = humanMessage(humanText);
177
+ const recall = recallMessage('recall placeholder');
178
+ const decision = { kind: 'continue', messages: [human, recall] };
179
+ const handler = preStepHandlers[0];
180
+ counters.preStepCalls += 1;
181
+ const result = await handler(payload, async () => decision);
182
+ return { result, human, recall };
183
+ };
184
+
185
+ // 第一轮:无帧 → 建帧(E_NO_FRAME 是真实首轮状态,随后 updateFrameFromMessages 落帧)。
186
+ const first = await invokeTurn(1, '我们继续聊活动流和角色内核,先看工作帧注入。');
187
+ const framePath = path.join(memoryRoot, '.longlive', 'working', `${sessionId}.json`);
188
+ const framePresentAfterTurn1 = fs.existsSync(framePath);
189
+ push('turnProcessed', first.result !== null && counters.preStepCalls >= 1, counters.preStepCalls);
190
+
191
+ // 第二轮:已有帧 → 真实注入(composePreStepBlocks + insertWorkingMessage),并合并焦点。
192
+ const second = await invokeTurn(2, '活动流注入顺序和角色内核的帧焦点继续。');
193
+ const messages = Array.isArray(second.result?.messages) ? second.result.messages : [];
194
+ const injectedIndex = messages.findIndex((message) => messageText(message).includes('【工作帧】'));
195
+ const recallIndex = messages.findIndex((message) => messageText(message).trimStart().startsWith(AUTO_TEXT_MARK.trimStart()));
196
+ if (injectedIndex >= 0) counters.injections += 1;
197
+
198
+ push('frameFilePresent', framePresentAfterTurn1 && fs.existsSync(framePath), framePath);
199
+ const sidecarPath = `${framePath}${SIDECAR_SUFFIX}`;
200
+ const verify = verifyProtectedFile({ memoryRoot, file: framePath });
201
+ push('hmacSidecarValid', fs.existsSync(sidecarPath) && verify.ok === true, verify.code ?? 'ok');
202
+
203
+ const loaded = store.load(sessionId);
204
+ push('frameLoaded', loaded.present === true && loaded.frame !== null, loaded.code ?? 'loaded');
205
+ const injectionAfterTurn = store.frameForInjection({ sessionId });
206
+ counters.updateFocusRuns = 1; // second turn hit updateFrameFromMessages → updateFocus (merge)
207
+ push('eNoFrameAbsentAfterTurn', injectionAfterTurn.inject === true && injectionAfterTurn.code !== 'E_NO_FRAME', injectionAfterTurn.code ?? 'inject');
208
+
209
+ const injectedText = injectedIndex >= 0 ? messageText(messages[injectedIndex]) : '';
210
+ push('injectionTextInjected', injectedText.trim() !== '' && injectedText.includes('【工作帧】'), injectedText.slice(0, 100));
211
+ push('injectionBeforeRecall', injectedIndex >= 0 && recallIndex >= 0 && injectedIndex < recallIndex, { injectedIndex, recallIndex });
212
+ push('focusUpdated', Array.isArray(loaded.frame?.focus?.entities)
213
+ && loaded.frame.focus.entities.length > 0
214
+ && Number(loaded.frame?.turnRange?.[1] ?? 0) >= 2, loaded.frame?.focus ?? null);
215
+
216
+ // 篡改 fail-safe:改帧文件 1 字节 → 注入/加载 fail closed;恢复 → 重新可注入。
217
+ const sealed = fs.readFileSync(framePath, 'utf8');
218
+ fs.writeFileSync(framePath, sealed.replace('"state": "active"', '"state": "stale"'), 'utf8');
219
+ const tampered = store.frameForInjection({ sessionId });
220
+ if (tampered.inject === false && tampered.code === 'E_INTEGRITY_TAMPER') counters.tamperRejections += 1;
221
+ push('tamperFailSafe', tampered.inject === false && tampered.code === 'E_INTEGRITY_TAMPER', tampered.code ?? 'inject');
222
+ fs.writeFileSync(framePath, sealed, 'utf8');
223
+ const restored = store.frameForInjection({ sessionId });
224
+ push('restoredInjecting', restored.inject === true, restored.code ?? 'inject');
225
+
226
+ const ok = checks.every((check) => check.ok === true);
227
+ return {
228
+ schema: FRAME_ACTIVATION_SCHEMA,
229
+ ok,
230
+ enabled: true,
231
+ killSwitch: false,
232
+ checks,
233
+ counters,
234
+ injectedText,
235
+ frame: loaded.frame ?? null,
236
+ files: { frame: framePath, sidecar: sidecarPath, workingDir: path.dirname(framePath) },
237
+ hooks: ['agent/pre-step'],
238
+ registered: Array.isArray(registered?.enabled) ? registered.enabled : [],
239
+ memoryRoot,
240
+ warnings,
241
+ };
242
+ } finally {
243
+ if (!keep) {
244
+ try { fs.rmSync(memoryRoot, { recursive: true, force: true }); } catch { /* temp */ }
245
+ }
246
+ }
247
+ }
@@ -0,0 +1,174 @@
1
+ // src/memory/fts-query.js — M2-A/K2:FTS5 查询构造与文本预分词
2
+ //
3
+ // 依据 `evidence/FTS5-MEMGAP-EVIDENCE.md`(任务 实测):
4
+ // * 裸 query 直接进 MATCH 必炸(`1.6.8` → syntax error near "."、`session-c045`
5
+ // → no such column、`- " ( ) * OR AND NOT` 各有固定语法错);
6
+ // * trigram tokenizer 覆盖中文 ≥3 字子串 / 版本号 / ID / 日期,但 2 字 CJK 与
7
+ // 2 字 ASCII 不命中;
8
+ // * unicode61 只匹配完整 token,需要**预分词列**:bigram + 别名(2 字 CJK 靠它)。
9
+ //
10
+ // 本模块只做「把任意用户问题变成若干条可安全执行的 MATCH 候选」,不打开数据库:
11
+ // buildFtsCandidates(text) → { raw: [{label, match}], terms: [{label, match}] }
12
+ // 调用方(event-index)对每条候选 try/catch + 参数绑定,全部失败/无命中 → NO_MATCH。
13
+ //
14
+ // 安全规则:NFKC → 去控制符 → 双引号剥除 → 所有候选一律「整段双引号包裹」;
15
+ // FTS 运算符词(OR/AND/NOT/NEAR)因此都按字面词处理,不进入语法层。
16
+
17
+ export const MAX_QUERY_CHARS = 200;
18
+
19
+ const CONTROL_RE = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g;
20
+ const ASCII_WORD_RE = /[A-Za-z0-9_][A-Za-z0-9_.:/\\-]*/g;
21
+ const CJK_RUN_RE = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]+/g;
22
+ const VERSION_RE = /\bv?\d+(?:\.\d+){1,3}\b/g;
23
+ const CJK_TOKEN_RE = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff]+/g;
24
+
25
+ /** NFKC + strip control chars + drop double quotes + collapse whitespace. */
26
+ export function normalizeQuery(input) {
27
+ const text = String(input ?? '');
28
+ let out = '';
29
+ try {
30
+ out = text.normalize('NFKC');
31
+ } catch {
32
+ out = text;
33
+ }
34
+ return out
35
+ .replace(CONTROL_RE, ' ')
36
+ .replace(/["\u201c\u201d\u2018\u2019]/g, ' ')
37
+ .replace(/\s+/g, ' ')
38
+ .trim()
39
+ .slice(0, MAX_QUERY_CHARS);
40
+ }
41
+
42
+ /** All CJK bigrams of a run (2-char windows, deduped, order preserved). */
43
+ export function cjkBigrams(run) {
44
+ const chars = Array.from(String(run ?? ''));
45
+ const out = [];
46
+ const seen = new Set();
47
+ for (let i = 0; i + 1 < chars.length; i += 1) {
48
+ const bigram = chars[i] + chars[i + 1];
49
+ if (!seen.has(bigram)) {
50
+ seen.add(bigram);
51
+ out.push(bigram);
52
+ }
53
+ }
54
+ return out;
55
+ }
56
+
57
+ /** Pre-tokenized term column text for the unicode61 shadow table. */
58
+ export function termTextOf(text) {
59
+ const normalized = normalizeQuery(text);
60
+ if (normalized === '') return '';
61
+ const tokens = [];
62
+ const seen = new Set();
63
+ const push = (token) => {
64
+ if (token === '' || seen.has(token)) return;
65
+ seen.add(token);
66
+ tokens.push(token);
67
+ };
68
+ for (const run of normalized.match(CJK_RUN_RE) ?? []) {
69
+ for (const bigram of cjkBigrams(run)) push(bigram);
70
+ }
71
+ for (const word of normalized.match(ASCII_WORD_RE) ?? []) push(word.toLowerCase());
72
+ return tokens.join(' ');
73
+ }
74
+
75
+ /** Alias expansions: versions get a `v`-twin, hyphen ids get their parts. */
76
+ export function aliasesOf(text) {
77
+ const normalized = normalizeQuery(text);
78
+ const out = new Set();
79
+ for (const match of normalized.match(VERSION_RE) ?? []) {
80
+ if (match.startsWith('v')) out.add(match.slice(1));
81
+ else out.add(`v${match}`);
82
+ }
83
+ for (const word of normalized.match(ASCII_WORD_RE) ?? []) {
84
+ const lower = word.toLowerCase();
85
+ if (lower.length >= 3 && lower.includes('-')) {
86
+ const parts = lower.split('-').filter((part) => part !== '');
87
+ if (parts.length >= 2) {
88
+ out.add(parts[parts.length - 1]);
89
+ out.add(parts.slice(0, 2).join('-'));
90
+ }
91
+ }
92
+ }
93
+ return [...out];
94
+ }
95
+
96
+ function escapeLiteral(text) {
97
+ return `"${String(text ?? '').replace(/"/g, '')}"`;
98
+ }
99
+
100
+ function segmentsOf(normalized) {
101
+ return normalized
102
+ .split(/[\s,;,;。!?!?、]+/)
103
+ .map((part) => part.trim())
104
+ .filter((part) => part !== '' && part !== '"');
105
+ }
106
+
107
+ function uniqueCandidates(items) {
108
+ const out = [];
109
+ const seen = new Set();
110
+ for (const item of items) {
111
+ if (!item || typeof item.match !== 'string' || item.match === '') continue;
112
+ if (seen.has(item.match)) continue;
113
+ seen.add(item.match);
114
+ out.push(item);
115
+ }
116
+ return out;
117
+ }
118
+
119
+ /**
120
+ * Build safe MATCH candidates for both FTS tables.
121
+ *
122
+ * raw = events_fts (trigram): 整句 phrase → 分段 AND → 分段 OR
123
+ * terms= events_terms (unicode61): bigram/word/alias AND → OR → 整句 phrase
124
+ */
125
+ export function buildFtsCandidates(input) {
126
+ const normalized = normalizeQuery(input);
127
+ if (normalized === '') return { normalized, raw: [], terms: [] };
128
+
129
+ const segments = segmentsOf(normalized);
130
+ const raw = uniqueCandidates([
131
+ { label: 'phrase', match: escapeLiteral(normalized) },
132
+ segments.length > 1
133
+ ? { label: 'and-segments', match: segments.map(escapeLiteral).join(' AND ') }
134
+ : null,
135
+ segments.length > 1
136
+ ? { label: 'or-segments', match: segments.map(escapeLiteral).join(' OR ') }
137
+ : null,
138
+ ]);
139
+
140
+ const words = (normalized.match(ASCII_WORD_RE) ?? []).map((word) => word.toLowerCase());
141
+ const bigrams = [];
142
+ for (const run of normalized.match(CJK_RUN_RE) ?? []) bigrams.push(...cjkBigrams(run));
143
+ const aliases = aliasesOf(normalized);
144
+ const tokens = [...new Set([...bigrams, ...words, ...aliases])].filter((token) => token.length >= 1);
145
+ const orderedTokens = tokens.slice(0, 24);
146
+ const terms = uniqueCandidates([
147
+ orderedTokens.length > 1
148
+ ? { label: 'and-terms', match: orderedTokens.map(escapeLiteral).join(' AND ') }
149
+ : null,
150
+ orderedTokens.length > 0
151
+ ? { label: 'or-terms', match: orderedTokens.map(escapeLiteral).join(' OR ') }
152
+ : null,
153
+ orderedTokens.length > 1
154
+ ? { label: 'or-terms-bigram', match: bigrams.map(escapeLiteral).join(' OR ') }
155
+ : null,
156
+ { label: 'phrase', match: escapeLiteral(normalized) },
157
+ ]);
158
+
159
+ return { normalized, raw, terms };
160
+ }
161
+
162
+ /** Extract the meaningful CJK/ASCII terms kept in EventRecord.terms. */
163
+ export function extractTerms(text, extra = []) {
164
+ const normalized = normalizeQuery(text);
165
+ const out = new Set(extra.filter((value) => typeof value === 'string' && value.trim() !== ''));
166
+ for (const match of normalized.match(VERSION_RE) ?? []) out.add(match);
167
+ for (const match of normalized.match(/\d{4}-\d{2}-\d{2}/g) ?? []) out.add(match);
168
+ for (const match of normalized.match(/\b\d{1,2}:\d{2}(?::\d{2})?\b/g) ?? []) out.add(match);
169
+ for (const match of normalized.match(/\b(?:session|task|evt|rec|rv)[-_:][A-Za-z0-9-]{2,}\b/gi) ?? []) out.add(match);
170
+ for (const run of normalized.match(CJK_TOKEN_RE) ?? []) {
171
+ if (Array.from(run).length >= 2 && Array.from(run).length <= 12) out.add(run);
172
+ }
173
+ return [...out].slice(0, 60);
174
+ }