@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,425 @@
1
+ // src/role/index.js — 角色内核装配(v2.8 §16.3;1.8.0 默认 ON + 独立 kill switch)
2
+ //
3
+ // 1.8.0 角色内核 = attention queue + channel adapter + relationship graph +
4
+ // personality projection + 三接口(src/role/interfaces.js)。
5
+ //
6
+ // 激活纪律(任务「功能没跑起来就等于没实现」):
7
+ // - `role.enabled` 由 config schema 默认 **true**(1.8.0 默认 ON);显式
8
+ // `role.enabled === false` 是**独立 kill switch**:不注册 hook、不建目录、零 IO。
9
+ // - 开启后按子开关注册,且每个组件都有**运行期真实调用**(不是只有测试直接调 readers):
10
+ // attention.enabled -> session/event 生产者:真事件 → attention.jsonl 行(P0/P1/P2)
11
+ // relationships.enabled-> 同一 hook 在 user/message 上真实做 tier/recall-weight 查询
12
+ // channels.enabled -> 同一 hook touch 宿主持久渠道(lastSeenAt 落盘)+ 注册表
13
+ // personality.enabled -> 首次生成快照 + systemPrompt.section 语气投影(每次取文真实读快照)
14
+ // - registerRole 返回 `runtime` 计数器 + `probe()`:给 scripts/verify-activation.mjs 做
15
+ // 「开关 on / 真实调用 > 0 / 文件已落盘 / 注入 section 非空」的激活证据,而非单测自证。
16
+ // - 所有 hook 内部 try/catch:角色层故障不得拖垮一个 turn。
17
+
18
+ import { createAttentionQueue, attentionPriorityOfKind, orderOpsItems, ATTENTION_DEFAULTS } from './attention.js';
19
+ import { createChannelRegistry, normalizeInboundMessage, CHANNEL_DEFAULTS } from './channels.js';
20
+ import { generatePersonalitySnapshot, readPersonality, toneProjection, PERSONALITY_DEFAULTS } from './personality.js';
21
+ import { readRelationships, RELATIONSHIP_DEFAULTS } from './relationships.js';
22
+ import { createRoleInterfaces, INTERFACE_CONTRACTS } from './interfaces.js';
23
+ import { iso, nowMs, tryRolePaths } from './store.js';
24
+ import { readAttentionView, readChannelsView } from './readers.js';
25
+ import fs from 'node:fs';
26
+
27
+ export const ROLE_FEATURE_NAME = 'role';
28
+ export const ROLE_SCHEMA_VERSION = 1;
29
+ export const ROLE_DEFAULTS = Object.freeze({
30
+ // 1.8.0 默认 ON(任务):功能必须真的跑起来;kill switch = 显式 false。
31
+ enabled: true,
32
+ attention: Object.freeze({ enabled: true, ...ATTENTION_DEFAULTS }),
33
+ channels: Object.freeze({ enabled: true }),
34
+ relationships: Object.freeze({ enabled: true }),
35
+ personality: Object.freeze({ enabled: true, profilePaths: PERSONALITY_DEFAULTS.profilePaths }),
36
+ });
37
+ /** 启用角色内核时默认注册的宿主内 presence 渠道(没有 send,发送会降级)。 */
38
+ export const DEFAULT_HOST_CHANNELS = Object.freeze([
39
+ Object.freeze({ id: 'web', type: 'web', capabilities: ['presence'] }),
40
+ Object.freeze({ id: 'tool:host', type: 'tool', capabilities: ['presence'], meta: { channelKind: 'host-tool' } }),
41
+ ]);
42
+
43
+ function bool(value, fallback) {
44
+ if (value === undefined) return fallback;
45
+ return value === true;
46
+ }
47
+
48
+ function clampInt(value, fallback, min, max) {
49
+ const number = Number(value);
50
+ if (!Number.isFinite(number)) return fallback;
51
+ return Math.min(max, Math.max(min, Math.floor(number)));
52
+ }
53
+
54
+ /** Normalize the `role` config block (does not touch normalizeFeatureConfig's shape). */
55
+ export function normalizeRoleConfig(config = {}) {
56
+ const value = config && typeof config === 'object' ? config : {};
57
+ const role = value.role && typeof value.role === 'object' ? value.role : {};
58
+ const attention = role.attention && typeof role.attention === 'object' ? role.attention : {};
59
+ const channels = role.channels && typeof role.channels === 'object' ? role.channels : {};
60
+ const relationships = role.relationships && typeof role.relationships === 'object' ? role.relationships : {};
61
+ const personality = role.personality && typeof role.personality === 'object' ? role.personality : {};
62
+ const profilePaths = Array.isArray(personality.profilePaths)
63
+ ? personality.profilePaths.filter((item) => typeof item === 'string' && item.trim() !== '')
64
+ : [...ROLE_DEFAULTS.personality.profilePaths];
65
+ return {
66
+ // kill switch:只有显式 false 才关闭(默认 ON;字符串/缺省一律按开)。
67
+ enabled: role.enabled === false ? false : ROLE_DEFAULTS.enabled,
68
+ attention: {
69
+ enabled: bool(attention.enabled, ROLE_DEFAULTS.attention.enabled),
70
+ maxItems: clampInt(attention.maxItems, ATTENTION_DEFAULTS.maxItems, 1, 500),
71
+ agingMs: clampInt(attention.agingMs, ATTENTION_DEFAULTS.agingMs, 0, 7 * 24 * 60 * 60 * 1000),
72
+ demoteSteps: clampInt(attention.demoteSteps, ATTENTION_DEFAULTS.demoteSteps, 0, 2),
73
+ injectMaxItems: clampInt(attention.injectMaxItems, ATTENTION_DEFAULTS.injectMaxItems, 1, 20),
74
+ },
75
+ channels: { enabled: bool(channels.enabled, ROLE_DEFAULTS.channels.enabled) },
76
+ relationships: { enabled: bool(relationships.enabled, ROLE_DEFAULTS.relationships.enabled) },
77
+ personality: {
78
+ enabled: bool(personality.enabled, ROLE_DEFAULTS.personality.enabled),
79
+ profilePaths,
80
+ },
81
+ };
82
+ }
83
+
84
+ function safeMessage(event) {
85
+ const data = event?.data ?? {};
86
+ const message = data.message && typeof data.message === 'object' ? data.message : data;
87
+ return { data, message };
88
+ }
89
+
90
+ /**
91
+ * Turn one host `session/event` into an attention row. Producers (v2.8 ①):
92
+ * user/message -> P1 kind=user subject=turn:<turn>
93
+ * tool/result error -> P0 kind=error subject=tool:<name>
94
+ * turn/end failed -> P0 kind=ops subject=turn:<turn>:<reason>
95
+ * turn/end completed-> P2 kind=session subject=turn:<turn>:completed
96
+ * Returns the enqueue result or null when the event is not an attention event.
97
+ */
98
+ export function ingestSessionEvent(queue, session, event, now = () => new Date()) {
99
+ const type = event?.type;
100
+ const turn = Number.isInteger(event?.data?.turn) ? event.data.turn : (Number.isInteger(event?.turn) ? event.turn : 0);
101
+ const at = iso(nowMs(now()));
102
+ const sessionId = typeof session?.id === 'string' ? session.id : (typeof session?.header?.id === 'string' ? session.header.id : '');
103
+ if (type === 'user/message') {
104
+ return queue.enqueue({ kind: 'user', priority: 'P1', subject: `turn:${turn}`, source: `session/user${sessionId ? `:${sessionId}` : ''}`, at });
105
+ }
106
+ if (type === 'tool/result') {
107
+ const { data, message } = safeMessage(event);
108
+ const failed = message?.isError === true || data.isError === true;
109
+ if (!failed) return null;
110
+ const toolName = typeof data.name === 'string' && data.name !== '' ? data.name : 'unknown';
111
+ return queue.enqueue({ kind: 'error', priority: 'P0', subject: `tool:${toolName}`, source: `session/tool${sessionId ? `:${sessionId}` : ''}`, at });
112
+ }
113
+ if (type === 'turn/end') {
114
+ const reason = event?.data?.reason?.kind ?? 'completed';
115
+ const failed = reason !== 'completed';
116
+ return queue.enqueue({
117
+ kind: failed ? 'ops' : 'session',
118
+ priority: failed ? 'P0' : 'P2',
119
+ subject: `turn:${turn}:${reason}`,
120
+ source: `session/turn${sessionId ? `:${sessionId}` : ''}`,
121
+ at,
122
+ });
123
+ }
124
+ return null;
125
+ }
126
+
127
+ /** Convenience producer for memory write/recall events (`kind=ops` interface). */
128
+ export function recordAttentionEvent({ memoryRoot, now = () => new Date(), config = {}, priority, kind = 'ops', subject = '', source = 'memory' } = {}) {
129
+ const queue = createAttentionQueue({ memoryRoot, now, config });
130
+ return queue.enqueue({ priority, kind, subject, source });
131
+ }
132
+
133
+ /** Runtime identity for the relationship bridge (host/user best effort, deterministic fallback). */
134
+ function runtimePersonId(session, event) {
135
+ const data = event?.data ?? {};
136
+ const candidates = [
137
+ data.userId,
138
+ data.personId,
139
+ data.actor,
140
+ session?.header?.userId,
141
+ session?.header?.actor,
142
+ session?.userId,
143
+ ];
144
+ for (const value of candidates) {
145
+ if (typeof value === 'string' && value.trim() !== '') return value.trim();
146
+ }
147
+ return 'user:host';
148
+ }
149
+
150
+ /**
151
+ * Register the role kernel on a host context.
152
+ * 1.8.0 默认 ON(Config schema role.enabled=true);显式 false 是独立 kill switch,
153
+ * 关闭时零 hook / 零目录 / 零 IO。runtime/probe() 提供运行期真实调用证据。
154
+ * @returns {{enabled:boolean, hooks:string[], modules:object, config:object, reason?:string, dispose:Function}}
155
+ */
156
+ export function registerRole(ctx, config = {}, options = {}) {
157
+ const cfg = normalizeRoleConfig(config);
158
+ const memoryRoot = typeof options.memoryRoot === 'string' && options.memoryRoot.trim() !== ''
159
+ ? options.memoryRoot.trim()
160
+ : null;
161
+ const now = typeof options.now === 'function' ? options.now : () => new Date();
162
+ const warn = typeof options.warn === 'function' ? options.warn : () => {};
163
+ // §16.8:role/** 宿主私有写保护(auditKey 可注入;默认走 <memoryRoot>/.longlive/.hostkey)。
164
+ const auditKey = options.auditKey ?? null;
165
+ const actorKind = typeof options.actorKind === 'string' ? options.actorKind : 'host';
166
+ const caller = options.caller ?? null;
167
+ const subject = options.subject ?? undefined;
168
+ const hooks = [];
169
+ const disposers = [];
170
+ const modules = { interfaces: null, attention: null, channels: null };
171
+ const noop = () => {};
172
+
173
+ if (!cfg.enabled) return { enabled: false, hooks, modules, config: cfg, reason: 'disabled', dispose: noop };
174
+ if (memoryRoot === null) {
175
+ return { enabled: false, hooks, modules, config: cfg, reason: 'no-memory-root', dispose: noop };
176
+ }
177
+
178
+  modules.interfaces = createRoleInterfaces({ memoryRoot, now, caller, auditKey });
179
+
180
+ // 运行期真实调用计数器(任务 激活证据):只由真实 hook / 注入回调递增。
181
+ const runtime = {
182
+ eventsSeen: 0,
183
+ attentionEnqueued: 0,
184
+ relationshipLookups: 0,
185
+ channelTouches: 0,
186
+ personalityProjections: 0,
187
+ hooks: 0,
188
+ lastRelationship: null,
189
+ lastAttentionSubject: null,
190
+ lastProducedAt: null,
191
+ };
192
+
193
+ // ③ relationship graph:只读装配;写入走 G7 relationship.update。
194
+ if (cfg.relationships.enabled) {
195
+ modules.relationships = {
196
+ read: () => readRelationships({ memoryRoot }),
197
+ tierFor: modules.interfaces.relationship.tierOf,
198
+ recallWeightFor: modules.interfaces.relationship.recallWeightFor,
199
+ multiplierForTier: modules.interfaces.relationship.multiplierForTier,
200
+ defaults: RELATIONSHIP_DEFAULTS,
201
+ };
202
+ }
203
+
204
+ // ② channel adapter:宿主内注册表 + 能力协商 + 降级。
205
+ if (cfg.channels.enabled) {
206
+ const registry = createChannelRegistry({ memoryRoot, now, auditKey, actorKind });
207
+ modules.channels = registry;
208
+ const candidates = [...DEFAULT_HOST_CHANNELS, ...(Array.isArray(options.channels) ? options.channels : [])];
209
+ for (const candidate of candidates) {
210
+ try {
211
+ registry.registerAdapter(candidate);
212
+ } catch (error) {
213
+ warn(`[role] 渠道注册失败 ${candidate?.id ?? '?'}(${error?.code ?? error?.message ?? error})`);
214
+ }
215
+ }
216
+ if (typeof options.channelSend === 'function') {
217
+ try {
218
+ registry.registerAdapter({
219
+ id: 'tool:host',
220
+ type: 'tool',
221
+ capabilities: ['send', 'attachments'],
222
+ send: options.channelSend,
223
+ meta: { channelKind: 'host-tool' },
224
+ });
225
+ } catch (error) {
226
+ warn(`[role] 宿主 send 适配器注册失败(${error?.code ?? error?.message ?? error})`);
227
+ }
228
+ }
229
+ }
230
+
231
+ // ① attention queue:真实 session/event 生产者(hook 在 unified bridge 注册)。
232
+ if (cfg.attention.enabled) {
233
+ modules.attention = createAttentionQueue({ memoryRoot, now, config: cfg.attention, auditKey, actorKind });
234
+ }
235
+
236
+ // 统一 runtime bridge:真实 session/event → attention 入队 + relationship 查询 + channel touch。
237
+ // 「运行期真实调用」证据只在这里递增;测试直接调 readers 不计入。
238
+ const queue = modules.attention;
239
+ const bridgeEnabled = Boolean(queue) || Boolean(modules.relationships) || Boolean(modules.channels);
240
+ if (bridgeEnabled) {
241
+ const bridge = (session, event) => {
242
+ runtime.eventsSeen += 1;
243
+ try {
244
+ if (queue) {
245
+ const enqueued = ingestSessionEvent(queue, session, event, now);
246
+ if (enqueued) {
247
+ runtime.attentionEnqueued += 1;
248
+ runtime.lastAttentionSubject = enqueued?.row?.subject ?? enqueued?.subject ?? null;
249
+ }
250
+ }
251
+ } catch {
252
+ /* 角色层故障不得拖垮 turn(attention 只是顺序偏好) */
253
+ }
254
+ try {
255
+ // relationship → recall weight:真实 graph 读取(未知身份按 stranger 返回,仍是真调用)。
256
+ if (modules.relationships && event?.type === 'user/message') {
257
+ const personId = runtimePersonId(session, event);
258
+ const weight = modules.interfaces.relationship.recallWeightFor(personId);
259
+ runtime.relationshipLookups += 1;
260
+ runtime.lastRelationship = { personId, tier: weight.tier, multiplier: weight.multiplier };
261
+ }
262
+ } catch {
263
+ /* best effort */
264
+ }
265
+ try {
266
+ // channel adapter:真实 touch 宿主持久渠道(lastSeenAt 落盘)。
267
+ if (modules.channels && typeof modules.channels.touch === 'function'
268
+ && (event?.type === 'user/message' || event?.type === 'turn/end')) {
269
+ const touched = modules.channels.touch('web', { at: now() });
270
+ if (touched?.ok === true) runtime.channelTouches += 1;
271
+ }
272
+ } catch {
273
+ /* best effort */
274
+ }
275
+ runtime.lastProducedAt = iso(nowMs(now()));
276
+ };
277
+ try {
278
+ if (ctx && typeof ctx.on === 'function') {
279
+ const disposer = ctx.on('session/event', bridge);
280
+ if (typeof disposer === 'function') disposers.push(disposer);
281
+ hooks.push('session/event');
282
+ runtime.hooks += 1;
283
+ }
284
+ } catch (error) {
285
+ warn(`[role] session/event bridge 注册失败(${error?.code ?? error?.message ?? error})`);
286
+ }
287
+ }
288
+
289
+ // ④ personality projection:首次生成快照 + 语气投影(systemPrompt.section)。
290
+ if (cfg.personality.enabled) {
291
+ try {
292
+ let current = readPersonality({ memoryRoot, caller, auditKey }).snapshot;
293
+ if (current === null || (current.version ?? 0) === 0) {
294
+ const generated = generatePersonalitySnapshot({
295
+ memoryRoot,
296
+ profilePaths: cfg.personality.profilePaths,
297
+ now,
298
+ auditKey,
299
+ actorKind,
300
+ subject,
301
+ });
302
+ current = generated.snapshot;
303
+ }
304
+ modules.personality = {
305
+ snapshot: () => readPersonality({ memoryRoot, caller, auditKey }).snapshot,
306
+ toneProjection: () => {
307
+ runtime.personalityProjections += 1;
308
+ return toneProjection({ snapshot: readPersonality({ memoryRoot, caller, auditKey }).snapshot });
309
+ },
310
+ retentionBiasFor: modules.interfaces.personality.retentionBiasFor,
311
+ decayThresholdsFor: modules.interfaces.personality.decayThresholdsFor,
312
+ };
313
+ const section = ctx?.systemPrompt;
314
+ if (section && typeof section.section === 'function') {
315
+ const initial = toneProjection({ snapshot: current });
316
+ if (initial.inject) {
317
+ const order = typeof section.getSectionOrder === 'function'
318
+ ? (section.getSectionOrder('TOOL_SUBAGENT') ?? 900)
319
+ : 900;
320
+ section.section({
321
+ name: 'role:tone',
322
+ order,
323
+ // 每次宿主取 section 文本都真实重读快照(并计入激活证据)。
324
+ text: () => {
325
+ runtime.personalityProjections += 1;
326
+ return toneProjection({ snapshot: readPersonality({ memoryRoot, caller, auditKey }).snapshot }).styleLine;
327
+ },
328
+ });
329
+ hooks.push('systemPrompt.section');
330
+ runtime.hooks += 1;
331
+ }
332
+ }
333
+ } catch (error) {
334
+ warn(`[role] personality 装配失败(${error?.code ?? error?.message ?? error})`);
335
+ }
336
+ }
337
+
338
+ /** 运行期激活证据快照:给 scripts/verify-activation.mjs / 宿主 doctor 用。 */
339
+ function probe() {
340
+ const attentionView = modules.attention ? readAttentionView({ memoryRoot, now }) : null;
341
+ const relationshipGraph = readRelationships({ memoryRoot });
342
+ const personalityRead = readPersonality({ memoryRoot, caller, auditKey });
343
+ let channelsView = null;
344
+ try {
345
+ channelsView = modules.channels && typeof modules.channels.view === 'function' ? modules.channels.view({ now }) : null;
346
+ } catch {
347
+ channelsView = null;
348
+ }
349
+ const paths = tryRolePaths(memoryRoot);
350
+ const files = paths
351
+ ? {
352
+ attention: fs.existsSync(paths.attention),
353
+ relationships: fs.existsSync(paths.relationships),
354
+ personality: fs.existsSync(paths.personality),
355
+ channels: fs.existsSync(paths.channels),
356
+ audit: fs.existsSync(paths.audit),
357
+ }
358
+ : {};
359
+ const checks = {
360
+ enabled: true,
361
+ hooksRegistered: runtime.hooks > 0,
362
+ eventsSeen: runtime.eventsSeen > 0,
363
+ attentionIngested: runtime.attentionEnqueued > 0,
364
+ relationshipRead: runtime.relationshipLookups > 0,
365
+ channelTouched: runtime.channelTouches > 0,
366
+ personalityProjected: runtime.personalityProjections > 0,
367
+ attentionFilePresent: files.attention === true,
368
+ channelsFilePresent: files.channels === true,
369
+ personalityFilePresent: files.personality === true,
370
+ };
371
+ return {
372
+ schema: 'role-activation/v1',
373
+ enabled: true,
374
+ hooks: [...hooks],
375
+ counters: { ...runtime },
376
+ checks,
377
+ attention: attentionView
378
+ ? {
379
+ present: attentionView.present,
380
+ count: attentionView.count,
381
+ code: attentionView.code,
382
+ // 前 10 行按优先级排序后的证据(verify-activation 断言 P0 先注入)。
383
+ items: (attentionView.items ?? []).slice(0, 10).map((item) => ({
384
+ priority: item.priority,
385
+ effectivePriority: item.effectivePriority,
386
+ kind: item.kind,
387
+ subject: item.subject,
388
+ })),
389
+ }
390
+ : null,
391
+ relationships: { present: relationshipGraph.present, count: relationshipGraph.count, code: relationshipGraph.code },
392
+ personality: {
393
+ present: personalityRead.present,
394
+ version: personalityRead.snapshot?.version ?? 0,
395
+ tone: personalityRead.snapshot?.tone ?? 'neutral',
396
+ },
397
+ channels: channelsView ? { present: channelsView.present, count: channelsView.count, code: channelsView.code } : null,
398
+ files,
399
+ readAt: iso(nowMs(now())),
400
+ };
401
+ }
402
+
403
+ function dispose() {
404
+ for (const disposer of disposers) {
405
+ try { disposer(); } catch { /* already disposed */ }
406
+ }
407
+ disposers.length = 0;
408
+ }
409
+ if (typeof ctx?.effect === 'function') {
410
+ try { ctx.effect(() => dispose, 'longlive.role'); } catch { /* fake ctx */ }
411
+ }
412
+ return { enabled: true, hooks, modules, config: cfg, dispose, runtime, probe };
413
+ }
414
+
415
+ export {
416
+ createAttentionQueue,
417
+ createChannelRegistry,
418
+ createRoleInterfaces,
419
+ generatePersonalitySnapshot,
420
+ normalizeInboundMessage,
421
+ attentionPriorityOfKind,
422
+ orderOpsItems,
423
+ INTERFACE_CONTRACTS,
424
+ CHANNEL_DEFAULTS,
425
+ };
@@ -0,0 +1,115 @@
1
+ // src/role/interfaces.js — 角色内核 · 三接口契约(v2.8 §16.3 接口接线)
2
+ //
3
+ // 三接口(字段名/类型/默认值/错误码在这里冻结,测试直接断言本文件的常量):
4
+ // 1) relationshipTier → recall 排序权重乘子(只排序,不改事实真值/保护谓词)
5
+ // 2) attention → kind=ops 优先级(只排序;与 recall 冲突时记忆事实优先)
6
+ // 3) personality snapshot → 遗忘偏好(retentionBias 只调阈值,不越权 protected/forbid)
7
+ //
8
+ // 消费方(记忆召回/遗忘/注入)可以直接 import 纯函数,也可以拿
9
+ // `createRoleInterfaces({memoryRoot})` 拿到与某个记忆根绑定的实现。
10
+
11
+ import { orderOpsItems, attentionPriorityOfKind, normalizeAttentionRow, ATTENTION_PRIORITIES } from './attention.js';
12
+ import {
13
+ RELATIONSHIP_TIERS, TIER_WEIGHTS, applyRelationshipWeights, readRelationships, recallWeightFor, relationshipTierMultiplier,
14
+ } from './relationships.js';
15
+ import {
16
+ PERSONALITY_SCHEMA, RETENTION_KINDS, applyRetentionBiasToRules, readPersonality, retentionBiasFor, toneProjection,
17
+ } from './personality.js';
18
+ import { nowMs, readJsonlSafe, tryRolePaths } from './store.js';
19
+
20
+ export const ROLE_INTERFACE_VERSION = '1.0.0';
21
+
22
+ /** 三接口契约描述(机器可读;测试用它锁字段名/类型/默认值/错误码)。 */
23
+ export const INTERFACE_CONTRACTS = Object.freeze({
24
+ relationshipTierToRecallWeight: Object.freeze({
25
+ id: 'relationshipTier->recallWeight',
26
+ version: ROLE_INTERFACE_VERSION,
27
+ fields: Object.freeze({
28
+ tier: Object.freeze({ type: 'enum', values: RELATIONSHIP_TIERS, default: 'stranger' }),
29
+ multiplier: Object.freeze({ type: 'number', default: TIER_WEIGHTS.stranger, table: TIER_WEIGHTS }),
30
+ }),
31
+ errors: Object.freeze(['E_RELATIONSHIP_TIER', 'E_ROLE_CAS_CONFLICT', 'E_ROLE_PARAMS']),
32
+ guarantees: Object.freeze(['只影响召回排序', '不改变事实真值/保护谓词']),
33
+ }),
34
+ attentionToOpsPriority: Object.freeze({
35
+ id: 'attention->kind=ops priority',
36
+ version: ROLE_INTERFACE_VERSION,
37
+ fields: Object.freeze({
38
+ priority: Object.freeze({ type: 'enum', values: ATTENTION_PRIORITIES, default: 'P2' }),
39
+ kind: Object.freeze({ type: 'enum', values: Object.freeze(['ops', 'user', 'tool', 'session', 'error']), default: 'ops' }),
40
+ subject: Object.freeze({ type: 'string', default: '' }),
41
+ }),
42
+ errors: Object.freeze(['E_ROLE_PARAMS']),
43
+ guarantees: Object.freeze(['只影响注入/面板顺序', '不改变召回内容', '与 recall 冲突时记忆事实优先']),
44
+ }),
45
+ personalitySnapshotToRetentionBias: Object.freeze({
46
+ id: 'personality snapshot->retention bias',
47
+ version: ROLE_INTERFACE_VERSION,
48
+ fields: Object.freeze({
49
+ snapshotId: Object.freeze({ type: 'string', default: null }),
50
+ version: Object.freeze({ type: 'number', default: 0 }),
51
+ retentionBias: Object.freeze({ type: 'map', keys: RETENTION_KINDS, value: Object.freeze({ type: 'number', default: 1.0, min: 0.8, max: 1.5 }) }),
52
+ tone: Object.freeze({ type: 'string', default: 'neutral' }),
53
+ createdAt: Object.freeze({ type: 'string', default: null }),
54
+ }),
55
+ errors: Object.freeze(['E_ROLE_PARAMS', 'E_ROLE_CORRUPT']),
56
+ guarantees: Object.freeze(['只影响衰减阈值', '不越权 protected/forbid/immuneSalience']),
57
+ }),
58
+ });
59
+
60
+ /** Read attention rows from disk without building the writer queue. */
61
+ export function readAttentionRows({ memoryRoot, limit = 10_000, auditKey = null } = {}) {
62
+ const paths = tryRolePaths(memoryRoot);
63
+ if (paths === null) return { rows: [], bad: 0, present: false, total: 0 };
64
+ const read = readJsonlSafe(paths.attention, { limit, memoryRoot, auditKey });
65
+ if (read.tampered === true) return { rows: [], bad: 0, present: false, total: 0, tampered: true, code: read.code ?? 'E_INTEGRITY_TAMPER' };
66
+ const rows = [];
67
+ let bad = 0;
68
+ for (const raw of read.rows) {
69
+ const row = normalizeAttentionRow(raw);
70
+ if (row) rows.push(row);
71
+ else bad += 1;
72
+ }
73
+ return { rows, bad, present: read.present, total: read.total ?? rows.length };
74
+ }
75
+
76
+ /**
77
+ * Bind the three interfaces to one memory root (read-side helpers).
78
+ * Writes stay in the owning modules (attention queue / updateRelationship /
79
+ * generatePersonalitySnapshot) so consumers cannot bypass validation.
80
+ */
81
+ export function createRoleInterfaces({ memoryRoot, now = () => new Date(), caller = null, auditKey = null } = {}) {
82
+ const clock = () => nowMs(now());
83
+ return {
84
+ version: ROLE_INTERFACE_VERSION,
85
+ memoryRoot,
86
+ caller,
87
+ contracts: INTERFACE_CONTRACTS,
88
+ relationship: {
89
+ tierOf: (personId) => {
90
+ const graph = readRelationships({ memoryRoot, caller, auditKey });
91
+ const hit = graph.nodes.find((node) => node.personId === String(personId ?? ''));
92
+ return hit ? hit.tier : 'stranger';
93
+ },
94
+ multiplierForTier: (tier) => relationshipTierMultiplier(tier),
95
+ recallWeightFor: (personId) => recallWeightFor({ memoryRoot, personId, caller, auditKey }),
96
+ applyWeights: (items, options = {}) => applyRelationshipWeights(items, { memoryRoot, caller, auditKey, ...options }),
97
+ },
98
+ attention: {
99
+ priorityOfKind: (kind) => attentionPriorityOfKind(kind),
100
+ orderOps: (items, options = {}) => {
101
+ const read = readAttentionRows({ memoryRoot, auditKey });
102
+ return orderOpsItems(items, read.rows, { now: options.now ?? clock() });
103
+ },
104
+ },
105
+ personality: {
106
+ read: () => readPersonality({ memoryRoot, caller, auditKey }),
107
+ snapshot: () => readPersonality({ memoryRoot, caller, auditKey }).snapshot,
108
+ retentionBiasFor: (kind) => retentionBiasFor(kind, { snapshot: readPersonality({ memoryRoot, caller, auditKey }).snapshot }),
109
+ decayThresholdsFor: (baseRules) => applyRetentionBiasToRules(baseRules, { snapshot: readPersonality({ memoryRoot, caller, auditKey }).snapshot }),
110
+ toneProjection: () => toneProjection({ snapshot: readPersonality({ memoryRoot, caller, auditKey }).snapshot }),
111
+ },
112
+ };
113
+ }
114
+
115
+ export { PERSONALITY_SCHEMA };