@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,318 @@
1
+ // src/role/attention.js — 角色内核 · attention queue(v2.8 §16.3 ①)
2
+ //
3
+ // 设计口径(§16.3):
4
+ // - 数据:`.longlive/role/attention.jsonl`,append-only,行
5
+ // `{id, priority:P0|P1|P2, kind:ops, subject, at, source}`;
6
+ // - pre-step 按 priority 决定事件注入顺序(P0 先);**不改变召回内容,只排序**;
7
+ // 与 recall 冲突时记忆事实优先(facts 由调用方排在最前,本模块只排 ops 项);
8
+ // - 接口:`attention → kind=ops 优先级`(导出 orderOpsItems / attentionPriorityOf)。
9
+ //
10
+ // 本文件补齐任务书要求的三件事:
11
+ // 1. 事件优先级权重:kind -> 基础 priority(ops/user→P1、tool/session→P2、error→P0),
12
+ // 显式 priority 优先;P0 抢占 = 排序时永远排在最前,与到达先后无关;
13
+ // 2. 队列容量:默认 50 行(`maxItems`),超出后压缩保留最新 50 行;
14
+ // 3. 老化:超过 `agingMs`(默认 30min)未消费的条目按 `demoteSteps` 降级
15
+ // (P0→P1→P2,最低 P2),排序时同优先级内老化项排在新鲜项之后。
16
+ //
17
+ // 纯本地、失败静默:写失败由调用方吞掉;读坏行只计数不抛。
18
+
19
+ import fs from 'node:fs';
20
+ import crypto from 'node:crypto';
21
+
22
+ import {
23
+ RoleError, ROLE_CODES, appendJsonl, ensureRoleDir, iso, nowMs, readJsonlSafe, rolePaths, writeJsonAtomic,
24
+ } from './store.js';
25
+ import { writeProtectedFile } from './writeprotect.js';
26
+
27
+ export const ATTENTION_SCHEMA = 'attention/v1';
28
+ export const ATTENTION_PRIORITIES = Object.freeze(['P0', 'P1', 'P2']);
29
+ export const ATTENTION_KINDS = Object.freeze(['ops', 'user', 'tool', 'session', 'error']);
30
+ /** 事件种类的基础优先级(显式 priority 覆盖它)。 */
31
+ export const KIND_BASE_PRIORITY = Object.freeze({
32
+ error: 'P0',
33
+ ops: 'P1',
34
+ user: 'P1',
35
+ tool: 'P2',
36
+ session: 'P2',
37
+ });
38
+ export const ATTENTION_DEFAULTS = Object.freeze({
39
+ maxItems: 50,
40
+ agingMs: 30 * 60 * 1000,
41
+ demoteSteps: 1,
42
+ injectMaxItems: 3,
43
+ });
44
+ const PRIORITY_INDEX = Object.freeze({ P0: 0, P1: 1, P2: 2 });
45
+
46
+ function clampInt(value, fallback, min, max) {
47
+ const number = Number(value);
48
+ if (!Number.isFinite(number)) return fallback;
49
+ return Math.min(max, Math.max(min, Math.floor(number)));
50
+ }
51
+
52
+ export function normalizeAttentionConfig(input = {}) {
53
+ const cfg = input && typeof input === 'object' ? input : {};
54
+ return {
55
+ maxItems: clampInt(cfg.maxItems, ATTENTION_DEFAULTS.maxItems, 1, 500),
56
+ agingMs: clampInt(cfg.agingMs, ATTENTION_DEFAULTS.agingMs, 0, 7 * 24 * 60 * 60 * 1000),
57
+ demoteSteps: clampInt(cfg.demoteSteps, ATTENTION_DEFAULTS.demoteSteps, 0, 2),
58
+ injectMaxItems: clampInt(cfg.injectMaxItems, ATTENTION_DEFAULTS.injectMaxItems, 1, 20),
59
+ };
60
+ }
61
+
62
+ export function isAttentionPriority(value) {
63
+ return ATTENTION_PRIORITIES.includes(value);
64
+ }
65
+
66
+ /** Priority for one event kind; unknown kinds fall back to P2 (never crash). */
67
+ export function attentionPriorityOfKind(kind) {
68
+ return KIND_BASE_PRIORITY[String(kind ?? '')] ?? 'P2';
69
+ }
70
+
71
+ export function newAttentionId(nowMsValue = Date.now()) {
72
+ return `att_${nowMsValue.toString(36)}_${crypto.randomBytes(4).toString('hex')}`;
73
+ }
74
+
75
+ /** Validate/normalize one stored row; returns null when it cannot be trusted. */
76
+ export function normalizeAttentionRow(raw) {
77
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
78
+ const id = typeof raw.id === 'string' && raw.id.trim() !== '' ? raw.id.trim() : null;
79
+ if (id === null) return null;
80
+ const priority = isAttentionPriority(raw.priority) ? raw.priority : null;
81
+ if (priority === null) return null;
82
+ const subject = typeof raw.subject === 'string' ? raw.subject : '';
83
+ const at = typeof raw.at === 'string' && Number.isFinite(Date.parse(raw.at)) ? raw.at : null;
84
+ if (at === null) return null;
85
+ return {
86
+ id,
87
+ priority,
88
+ kind: typeof raw.kind === 'string' && raw.kind !== '' ? raw.kind : 'ops',
89
+ subject,
90
+ at,
91
+ source: typeof raw.source === 'string' ? raw.source : 'role.attention',
92
+ seq: Number.isInteger(raw.seq) && raw.seq >= 0 ? raw.seq : 0,
93
+ };
94
+ }
95
+
96
+ /** Pure aging/demotion of one row; never mutates the stored row. */
97
+ export function ageAttentionRow(row, { now = Date.now(), agingMs = ATTENTION_DEFAULTS.agingMs, demoteSteps = ATTENTION_DEFAULTS.demoteSteps } = {}) {
98
+ const atMs = Date.parse(row.at);
99
+ const ageMs = Number.isFinite(atMs) ? Math.max(0, nowMs(now) - atMs) : 0;
100
+ const aged = agingMs > 0 && ageMs > agingMs;
101
+ const seed = PRIORITY_INDEX[row.priority] ?? PRIORITY_INDEX.P2;
102
+ const index = aged ? Math.min(PRIORITY_INDEX.P2, seed + demoteSteps) : seed;
103
+ return {
104
+ ...row,
105
+ ageMs,
106
+ aged,
107
+ effectivePriority: ATTENTION_PRIORITIES[index],
108
+ };
109
+ }
110
+
111
+ /**
112
+ * Stable injection order: effective priority first (P0 preempts), then fresh
113
+ * before aged, then arrival order (`at` asc, `seq` asc). Pure: no IO.
114
+ */
115
+ export function orderAttentionRows(rows, options = {}) {
116
+ const cfg = normalizeAttentionConfig(options.config ?? options);
117
+ const now = options.now ?? Date.now();
118
+ const decorated = (Array.isArray(rows) ? rows : [])
119
+ .map((row) => normalizeAttentionRow(row))
120
+ .filter(Boolean)
121
+ .map((row) => ageAttentionRow(row, { now, agingMs: cfg.agingMs, demoteSteps: cfg.demoteSteps }));
122
+ decorated.sort((left, right) => {
123
+ const byPriority = (PRIORITY_INDEX[left.effectivePriority] ?? 2) - (PRIORITY_INDEX[right.effectivePriority] ?? 2);
124
+ if (byPriority !== 0) return byPriority;
125
+ if (left.aged !== right.aged) return left.aged ? 1 : -1;
126
+ const byAt = String(left.at).localeCompare(String(right.at));
127
+ if (byAt !== 0) return byAt;
128
+ return (left.seq ?? 0) - (right.seq ?? 0) || left.id.localeCompare(right.id);
129
+ });
130
+ return decorated;
131
+ }
132
+
133
+ /**
134
+ * The `attention → kind=ops` interface (pure): reorder ops items by their
135
+ * attention priority. Facts are NOT handled here — the caller keeps memory
136
+ * facts ahead of ops items (与 recall 冲突时记忆事实优先).
137
+ * @param {Array<object>} opsItems items with `subject`/`id` matching attention rows
138
+ * @param {Array<object>} attentionRows rows from the queue
139
+ */
140
+ export function orderOpsItems(opsItems, attentionRows, options = {}) {
141
+ const items = Array.isArray(opsItems) ? opsItems : [];
142
+ const orderedRows = orderAttentionRows(attentionRows, options);
143
+ const priorityBySubject = new Map();
144
+ for (const row of orderedRows) {
145
+ const key = row.subject !== '' ? row.subject : row.id;
146
+ if (!priorityBySubject.has(key)) priorityBySubject.set(key, row.effectivePriority);
147
+ }
148
+ const decorated = items.map((item, index) => {
149
+ const key = typeof item?.subject === 'string' && item.subject !== ''
150
+ ? item.subject
151
+ : (typeof item?.id === 'string' ? item.id : '');
152
+ const explicit = isAttentionPriority(item?.attentionPriority) ? item.attentionPriority : null;
153
+ const priority = explicit ?? priorityBySubject.get(key) ?? 'P2';
154
+ return { item, index, priority, priorityIndex: PRIORITY_INDEX[priority] ?? 2 };
155
+ });
156
+ decorated.sort((left, right) => left.priorityIndex - right.priorityIndex || left.index - right.index);
157
+ return {
158
+ items: decorated.map((entry) => entry.item),
159
+ priorities: decorated.map((entry) => entry.priority),
160
+ unmatched: decorated.filter((entry) => !priorityBySubject.has(
161
+ typeof entry.item?.subject === 'string' && entry.item.subject !== ''
162
+ ? entry.item.subject
163
+ : (typeof entry.item?.id === 'string' ? entry.item.id : ''),
164
+ )).length,
165
+ source: 'attention',
166
+ };
167
+ }
168
+
169
+ /**
170
+ * Append-only attention queue. Writing is best effort at the call site; the
171
+ * queue itself validates arguments and throws RoleError on bad input (the
172
+ * feature wiring swallows that, tests assert it).
173
+ */
174
+ export function createAttentionQueue({ memoryRoot, now = () => new Date(), config = {}, auditKey = null, actorKind = 'host' } = {}) {
175
+ const cfg = normalizeAttentionConfig(config);
176
+ const root = memoryRoot;
177
+ const paths = rolePaths(root);
178
+
179
+ function readRows({ limit = 10_000 } = {}) {
180
+ const read = readJsonlSafe(paths.attention, { limit, memoryRoot: root, auditKey });
181
+ if (read.tampered === true) {
182
+ return { present: true, rows: [], bad: 0, total: 0, tampered: true, code: read.code ?? ROLE_CODES.TAMPER };
183
+ }
184
+ const rows = [];
185
+ for (const raw of read.rows) {
186
+ const row = normalizeAttentionRow(raw);
187
+ if (row) rows.push(row);
188
+ }
189
+ return { present: read.present, rows, bad: read.bad, total: read.total ?? rows.length, tampered: false };
190
+ }
191
+
192
+ function nextSeq(rows) {
193
+ let max = 0;
194
+ for (const row of rows) if (Number.isInteger(row.seq) && row.seq > max) max = row.seq;
195
+ return max + 1;
196
+ }
197
+
198
+ function compact(rows) {
199
+ if (rows.length <= cfg.maxItems) return { compacted: false, kept: rows.length };
200
+ const keptRows = orderAttentionRows(rows, { now: now(), config: cfg })
201
+ .sort((left, right) => (left.seq ?? 0) - (right.seq ?? 0))
202
+ .slice(-cfg.maxItems);
203
+ const text = `${keptRows.map((row) => JSON.stringify({
204
+ id: row.id,
205
+ priority: row.priority,
206
+ kind: row.kind,
207
+ subject: row.subject,
208
+ at: row.at,
209
+ source: row.source,
210
+ seq: row.seq,
211
+ })).join('\n')}\n`;
212
+ writeProtectedFile({ memoryRoot: root, file: paths.attention, text, auditKey, actorKind });
213
+ return { compacted: true, kept: keptRows.length };
214
+ }
215
+
216
+ /**
217
+ * Record one event. `priority` wins over the kind table; `kind` defaults to
218
+ * 'ops' (the v2.8 row schema names kind=ops explicitly).
219
+ */
220
+ function enqueue({
221
+ priority = undefined,
222
+ kind = 'ops',
223
+ subject = '',
224
+ source = 'role.attention',
225
+ at = undefined,
226
+ } = {}) {
227
+ const resolvedKind = ATTENTION_KINDS.includes(String(kind)) ? String(kind) : 'ops';
228
+ const resolvedPriority = priority === undefined ? attentionPriorityOfKind(resolvedKind) : priority;
229
+ if (!isAttentionPriority(resolvedPriority)) {
230
+ throw new RoleError(ROLE_CODES.PARAMS, `attention priority 非法:${JSON.stringify(priority)}(允许 ${ATTENTION_PRIORITIES.join('|')})`);
231
+ }
232
+ if (typeof subject === 'string' && Array.from(subject).length > 200) {
233
+ throw new RoleError(ROLE_CODES.PARAMS, 'attention subject 超过 200 字符');
234
+ }
235
+ const atMs = at === undefined
236
+ ? nowMs(now())
237
+ : (at instanceof Date ? at.getTime() : (typeof at === 'number' ? at : Date.parse(String(at))));
238
+ if (!Number.isFinite(atMs)) throw new RoleError(ROLE_CODES.PARAMS, 'attention.at 不是可解析的时间');
239
+ ensureRoleDir(root);
240
+ const existing = readRows();
241
+ const row = {
242
+ id: newAttentionId(atMs),
243
+ priority: resolvedPriority,
244
+ kind: resolvedKind,
245
+ subject: typeof subject === 'string' ? subject : '',
246
+ at: iso(atMs),
247
+ source: typeof source === 'string' && source !== '' ? source : 'role.attention',
248
+ seq: nextSeq(existing.rows),
249
+ };
250
+ appendJsonl(paths.attention, row, { memoryRoot: root, auditKey, actorKind });
251
+ const total = existing.rows.length + 1;
252
+ const compaction = total > cfg.maxItems ? compact([...existing.rows, row]) : { compacted: false, kept: total };
253
+ return { ok: true, row, seq: row.seq, compaction };
254
+ }
255
+
256
+ /** Rows in injection order (aged items demoted, P0 first). */
257
+ function pending({ limit = cfg.maxItems, now: nowValue = undefined } = {}) {
258
+ const read = readRows();
259
+ const ordered = orderAttentionRows(read.rows, { now: nowValue ?? now(), config: cfg });
260
+ return {
261
+ ok: read.tampered !== true,
262
+ items: ordered.slice(0, limit),
263
+ total: read.rows.length,
264
+ bad: read.bad,
265
+ present: read.present,
266
+ tampered: read.tampered === true,
267
+ code: read.tampered === true ? (read.code ?? ROLE_CODES.TAMPER) : null,
268
+ };
269
+ }
270
+
271
+ /** Read-only panel/route view (`attention/v1`). */
272
+ function view({ limit = cfg.maxItems, now: nowValue = undefined } = {}) {
273
+ const pendingItems = pending({ limit, now: nowValue });
274
+ return {
275
+ schema: ATTENTION_SCHEMA,
276
+ present: pendingItems.present,
277
+ count: pendingItems.items.length,
278
+ total: pendingItems.total,
279
+ bad: pendingItems.bad,
280
+ tampered: pendingItems.tampered === true,
281
+ degraded: pendingItems.tampered === true || pendingItems.bad > 0 || pendingItems.present === false,
282
+ items: pendingItems.items.map((row) => ({
283
+ id: row.id,
284
+ priority: row.priority,
285
+ kind: row.kind,
286
+ subject: row.subject,
287
+ at: row.at,
288
+ source: row.source,
289
+ effectivePriority: row.effectivePriority,
290
+ aged: row.aged,
291
+ })),
292
+ };
293
+ }
294
+
295
+ /** Highest effective priority matching a subject (null when unknown). */
296
+ function priorityFor(subject, { now: nowValue = undefined } = {}) {
297
+ const pendingItems = pending({ limit: cfg.maxItems, now: nowValue });
298
+ for (const row of pendingItems.items) if (row.subject === String(subject ?? '')) return row.effectivePriority;
299
+ return null;
300
+ }
301
+
302
+ /** The ops ordering interface bound to this queue (pure result). */
303
+ function orderOps(items, options = {}) {
304
+ const read = readRows();
305
+ return orderOpsItems(items, read.rows, { now: options.now ?? now(), config: cfg });
306
+ }
307
+
308
+ return {
309
+ enqueue,
310
+ pending,
311
+ view,
312
+ priorityFor,
313
+ orderOps,
314
+ config: cfg,
315
+ paths,
316
+ read: readRows,
317
+ };
318
+ }
@@ -0,0 +1,36 @@
1
+ // src/role/base.js — 角色内核基础类型(避免 store <-> writeprotect 循环依赖)
2
+ import crypto from 'node:crypto';
3
+
4
+ /** 角色层错误码(写路径抛 RoleError,读路径降级不抛)。 */
5
+ export const ROLE_CODES = Object.freeze({
6
+ PARAMS: 'E_ROLE_PARAMS',
7
+ DISABLED: 'E_ROLE_DISABLED',
8
+ CAS_CONFLICT: 'E_ROLE_CAS_CONFLICT',
9
+ CORRUPT: 'E_ROLE_CORRUPT',
10
+ PATH_OUTSIDE_ROOT: 'E_PROFILE_OUTSIDE_ROOT',
11
+ });
12
+
13
+ export class RoleError extends Error {
14
+ constructor(code, message, details = undefined) {
15
+ super(message);
16
+ this.name = 'RoleError';
17
+ this.code = code;
18
+ this.status = code === ROLE_CODES.CAS_CONFLICT ? 409 : 422;
19
+ if (details !== undefined) this.details = details;
20
+ }
21
+ }
22
+
23
+ export function nowMs(now) {
24
+ const value = typeof now === 'function' ? now() : now;
25
+ if (value instanceof Date) return value.getTime();
26
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
27
+ return Date.now();
28
+ }
29
+
30
+ export function iso(ms) {
31
+ return new Date(ms).toISOString();
32
+ }
33
+
34
+ export function sha256Hex(text) {
35
+ return crypto.createHash('sha256').update(String(text ?? ''), 'utf8').digest('hex');
36
+ }