@jjh32456/dsh-longlive 1.6.8 → 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 (288) hide show
  1. package/CHANGELOG.md +69 -1539
  2. package/LICENSE +1 -1
  3. package/QUICKSTART.md +33 -12
  4. package/README.en.md +17 -7
  5. package/README.md +29 -10
  6. package/SPEC.md +5 -5
  7. package/cordis.patch.yml +17 -12
  8. package/docs/ANSWER-GUARD.md +92 -0
  9. package/docs/ARCHITECTURE.md +3 -3
  10. package/docs/AUTO-RECALL-WIRING.md +1 -1
  11. package/docs/AUTO-RECALL.md +15 -9
  12. package/docs/CONFIG.md +143 -37
  13. package/docs/ERRORS.md +17 -5
  14. package/docs/FAQ.md +7 -7
  15. package/docs/FEATURES.md +66 -29
  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 +3 -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 +42 -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 +129 -18
  41. package/src/activity/model.js +11 -1
  42. package/src/activity/store.js +22 -3
  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 +145 -48
  58. package/src/deps.js +6 -4
  59. package/src/features.js +158 -8
  60. package/src/identity/index.js +13 -5
  61. package/src/identity/inject-position.js +2 -2
  62. package/src/index.js +97 -7
  63. package/src/lifecycle/gap-clocks.js +328 -0
  64. package/src/lifecycle/index.js +269 -0
  65. package/src/memory/activity-capture.js +229 -0
  66. package/src/memory/anchors.js +194 -0
  67. package/src/memory/audit-records.js +222 -0
  68. package/src/memory/auto-cards.js +401 -0
  69. package/src/memory/capture.js +442 -0
  70. package/src/memory/card-anchors.js +203 -0
  71. package/src/memory/conflicts.js +106 -0
  72. package/src/memory/decay.js +311 -0
  73. package/src/memory/distill.js +471 -0
  74. package/src/memory/event-index.js +917 -0
  75. package/src/memory/evidence-chain.js +295 -0
  76. package/src/memory/evidence.js +145 -0
  77. package/src/memory/filter-runner.js +1 -1
  78. package/src/memory/filter.js +5 -1
  79. package/src/memory/forget-v2.js +410 -0
  80. package/src/memory/forget.js +71 -0
  81. package/src/memory/frames-activation.js +247 -0
  82. package/src/memory/fts-query.js +174 -0
  83. package/src/memory/governance.js +604 -0
  84. package/src/memory/index.js +1 -0
  85. package/src/memory/l2-gate.js +169 -0
  86. package/src/memory/lock.js +121 -0
  87. package/src/memory/migrate.js +139 -0
  88. package/src/memory/recall-v2.js +223 -0
  89. package/src/memory/recall.js +65 -2
  90. package/src/memory/records.js +425 -0
  91. package/src/memory/retention.js +128 -0
  92. package/src/memory/review.js +124 -0
  93. package/src/memory/trust.js +88 -0
  94. package/src/memory/version-chain.js +264 -0
  95. package/src/memory/wal.js +137 -0
  96. package/src/memory/working-frame.js +516 -0
  97. package/src/metrics/classify.mjs +5 -3
  98. package/src/metrics/index.mjs +21 -1
  99. package/src/metrics/prestep-latency.mjs +300 -0
  100. package/src/metrics/store.mjs +25 -0
  101. package/src/policy-profile.js +18 -3
  102. package/src/recall/anaphora.mjs +87 -0
  103. package/src/recall/cards.mjs +7 -0
  104. package/src/recall/embedding.mjs +8 -8
  105. package/src/recall/entities.mjs +112 -0
  106. package/src/recall/index.js +235 -19
  107. package/src/recall/inject-order.mjs +10 -8
  108. package/src/recall/privacy.mjs +1 -1
  109. package/src/recall/recall-core.mjs +116 -10
  110. package/src/recall/shared-visibility.js +302 -0
  111. package/src/recall/time-normalizer.mjs +196 -0
  112. package/src/role/activation.js +191 -0
  113. package/src/role/attention.js +318 -0
  114. package/src/role/base.js +36 -0
  115. package/src/role/channels.js +423 -0
  116. package/src/role/index.js +425 -0
  117. package/src/role/interfaces.js +115 -0
  118. package/src/role/personality.js +370 -0
  119. package/src/role/readers.js +151 -0
  120. package/src/role/relationships.js +477 -0
  121. package/src/role/store.js +202 -0
  122. package/src/role/writeprotect.js +373 -0
  123. package/src/router/aliases.json +176 -25
  124. package/src/router/background/agent-runner.mjs +1 -1
  125. package/src/router/background/anchor.mjs +1 -1
  126. package/src/router/background/audit.mjs +2 -2
  127. package/src/router/background/budget-ledger.mjs +2 -2
  128. package/src/router/background/host-sim.mjs +1 -1
  129. package/src/router/background/memory.mjs +4 -4
  130. package/src/router/background/queue.mjs +1 -1
  131. package/src/router/background/retry.mjs +1 -1
  132. package/src/router/background/router.mjs +1 -1
  133. package/src/router/background/stream.mjs +1 -1
  134. package/src/router/background/tool-policy.mjs +1 -1
  135. package/src/router/design-headings.json +5 -5
  136. package/src/router/face/config.mjs +1 -1
  137. package/src/router/face/identity.mjs +3 -3
  138. package/src/router/judge.mjs +8 -7
  139. package/src/router/policies/generic.json +21 -4
  140. package/src/router/route.js +109 -22
  141. package/src/router/router.mjs +10 -6
  142. package/src/router/routing-rules.json +260 -445
  143. package/src/router/run-sim.mjs +8 -8
  144. package/src/router/runtime.js +23 -12
  145. package/src/router/service/config.mjs +5 -5
  146. package/src/router/service/context.mjs +1 -1
  147. package/src/router/service/queue.mjs +1 -1
  148. package/src/router/shadow.mjs +4 -3
  149. package/src/surgery-recovery.js +2 -2
  150. package/src/tool-output.js +2 -2
  151. package/src/tools.js +39 -7
  152. package/src/truncate-visible.js +2 -2
  153. package/src/ui/actions.js +1312 -0
  154. package/src/ui/panel-api.js +367 -0
  155. package/src/ui/panel-auth.js +251 -0
  156. package/src/ui/panel-integrity.js +243 -0
  157. package/src/ui/routes-readonly.js +695 -0
  158. package/src/ui/routes.js +3 -0
  159. package/docs/DEV.md +0 -279
  160. package/docs/PRESENCE-BUS-WIRING.md +0 -491
  161. package/docs/PRESENCE-BUS.md +0 -694
  162. package/docs/SURGERY.md +0 -186
  163. package/scripts/mark-core-cards.mjs +0 -289
  164. package/scripts/pack.mjs +0 -392
  165. package/scripts/router-proposals.mjs +0 -386
  166. package/src/router/fixtures/M1.json +0 -16
  167. package/src/router/fixtures/M2.json +0 -16
  168. package/src/router/fixtures/M3.json +0 -20
  169. package/src/router/fixtures/M4.json +0 -20
  170. package/src/router/fixtures/M5.json +0 -45
  171. package/src/router/fixtures/M6.json +0 -53
  172. package/src/router/fixtures/M7.json +0 -46
  173. package/src/router/fixtures/S1.json +0 -20
  174. package/src/router/fixtures/S2.json +0 -23
  175. package/src/router/fixtures/S3.json +0 -20
  176. package/src/router/fixtures/S4.json +0 -34
  177. package/src/router/fixtures/S5.json +0 -43
  178. package/src/router/fixtures/S6.json +0 -47
  179. package/src/router/fixtures/T1.json +0 -23
  180. package/src/router/fixtures/T2.json +0 -16
  181. package/src/router/fixtures/T3.json +0 -19
  182. package/src/router/fixtures/T4.json +0 -16
  183. package/src/router/fixtures/X1.json +0 -17
  184. package/src/router/fixtures/X2.json +0 -20
  185. package/src/router/gen-design-headings.mjs +0 -107
  186. package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
  187. package/test/activity-realtime.test.mjs +0 -959
  188. package/test/activity-stream.test.mjs +0 -512
  189. package/test/auto-recall.test.mjs +0 -474
  190. package/test/auto-truncate.test.mjs +0 -931
  191. package/test/autobind.test.mjs +0 -399
  192. package/test/bus.test.mjs +0 -1572
  193. package/test/cli-guard.test.mjs +0 -198
  194. package/test/conclusion.test.mjs +0 -408
  195. package/test/config-schema.test.mjs +0 -210
  196. package/test/d6-output.test.mjs +0 -194
  197. package/test/d6.test.mjs +0 -395
  198. package/test/deps.test.mjs +0 -342
  199. package/test/doctor-setup.test.mjs +0 -297
  200. package/test/embedding-recall.test.mjs +0 -347
  201. package/test/feature-flags.test.mjs +0 -585
  202. package/test/filter-shadow.test.mjs +0 -170
  203. package/test/fixtures/activity-append-child.mjs +0 -29
  204. package/test/fixtures/baseline-0.8.0-registration.json +0 -556
  205. package/test/fixtures/d6-header-tools.json +0 -1
  206. package/test/fixtures/lock-child.mjs +0 -67
  207. package/test/fixtures/pre-step-container.mjs +0 -100
  208. package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
  209. package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
  210. package/test/fixtures/profile-session-longlive-config.json +0 -20
  211. package/test/fixtures/proposal-cas-child.mjs +0 -66
  212. package/test/host-engine-status.test.mjs +0 -307
  213. package/test/hot-truncate.test.mjs +0 -1349
  214. package/test/identity-card-marker.test.mjs +0 -307
  215. package/test/identity-router/run.mjs +0 -721
  216. package/test/integration-pre-step.test.mjs +0 -553
  217. package/test/live.test.mjs +0 -1023
  218. package/test/maintenance-schedule.test.mjs +0 -538
  219. package/test/memory.test.mjs +0 -2828
  220. package/test/metrics.test.mjs +0 -478
  221. package/test/orchestrator.test.mjs +0 -266
  222. package/test/p1_probe.mjs +0 -97
  223. package/test/packaging.test.mjs +0 -184
  224. package/test/portability.test.mjs +0 -343
  225. package/test/rc4-regressions.test.mjs +0 -562
  226. package/test/rc5-regressions.test.mjs +0 -197
  227. package/test/rc6-chinese.test.mjs +0 -394
  228. package/test/router-1100/apply-off.test.mjs +0 -175
  229. package/test/router-1100/budget-reconcile.test.mjs +0 -215
  230. package/test/router-1100/config-context.test.mjs +0 -211
  231. package/test/router-1100/helpers.mjs +0 -129
  232. package/test/router-1100/hint-envelope.test.mjs +0 -150
  233. package/test/router-1100/lock-crossproc.test.mjs +0 -206
  234. package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
  235. package/test/router-1100/router-apply.test.mjs +0 -372
  236. package/test/router-1100/router-fixb.test.mjs +0 -602
  237. package/test/router-1100/router-proposals.test.mjs +0 -405
  238. package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
  239. package/test/router-1100/router-security-cr.test.mjs +0 -333
  240. package/test/router-1100/service-lifecycle.test.mjs +0 -220
  241. package/test/router-1100/tools-contract.test.mjs +0 -164
  242. package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
  243. package/test/router-1100/v6-contract.test.mjs +0 -158
  244. package/test/router-1100/v6b-contract.test.mjs +0 -293
  245. package/test/router-1100/v6c-contract.test.mjs +0 -249
  246. package/test/router-1100/wal-rotation.test.mjs +0 -375
  247. package/test/router-1100/wp1-config.test.mjs +0 -170
  248. package/test/router-1100/wp1-default-off.test.mjs +0 -77
  249. package/test/router-1100/wp1-model-status.test.mjs +0 -90
  250. package/test/router-1100/wp1-status.test.mjs +0 -220
  251. package/test/router-1100/wp1-tools.test.mjs +0 -146
  252. package/test/router-1100/wp5-cli.test.mjs +0 -240
  253. package/test/router-1100/wp5-model-auto.test.mjs +0 -418
  254. package/test/router-1100/wp6-merge.test.mjs +0 -386
  255. package/test/router-1100/wp6-wake.test.mjs +0 -502
  256. package/test/router-background/audit.test.mjs +0 -37
  257. package/test/router-background/budget-ledger.test.mjs +0 -330
  258. package/test/router-background/cross-process.test.mjs +0 -222
  259. package/test/router-background/fixtures/cross-child.mjs +0 -85
  260. package/test/router-background/fixtures/real-shapes.mjs +0 -239
  261. package/test/router-background/router.test.mjs +0 -1076
  262. package/test/router-background/stream-retry.test.mjs +0 -358
  263. package/test/router-background/tool-agent.test.mjs +0 -384
  264. package/test/router-face/bridge.test.mjs +0 -169
  265. package/test/router-face/face.test.mjs +0 -316
  266. package/test/router-failclosed.test.mjs +0 -466
  267. package/test/router-identity.test.mjs +0 -384
  268. package/test/router-l1-l2.contract.test.mjs +0 -1075
  269. package/test/router-memory-tools.test.mjs +0 -262
  270. package/test/router-policy.test.mjs +0 -225
  271. package/test/router-runtime.test.mjs +0 -438
  272. package/test/router-shadow.test.mjs +0 -287
  273. package/test/router.test.mjs +0 -903
  274. package/test/run-rc.mjs +0 -315
  275. package/test/run.mjs +0 -2469
  276. package/test/safety-defaults.test.mjs +0 -181
  277. package/test/seeded-load.test.mjs +0 -436
  278. package/test/session-original.test.mjs +0 -338
  279. package/test/state-truth.test.mjs +0 -937
  280. package/test/surgery-recovery.test.mjs +0 -936
  281. package/test/t7_drive.mjs +0 -151
  282. package/test/tool-output.test.mjs +0 -393
  283. package/test/tool-schema.test.mjs +0 -177
  284. package/test/tools_contract.test.mjs +0 -716
  285. package/test/truncate-visible.test.mjs +0 -1150
  286. package/test/ui-flags.test.mjs +0 -477
  287. package/test/v4-hardening.test.mjs +0 -427
  288. package/test/version-consistency.test.mjs +0 -286
@@ -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
+ }