@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.
- package/CHANGELOG.md +70 -1508
- package/LICENSE +1 -1
- package/QUICKSTART.md +34 -13
- package/README.en.md +18 -7
- package/README.md +34 -9
- package/SPEC.md +5 -5
- package/cordis.patch.yml +23 -9
- package/docs/ANSWER-GUARD.md +92 -0
- package/docs/ARCHITECTURE.md +4 -3
- package/docs/AUTO-RECALL-WIRING.md +1 -1
- package/docs/AUTO-RECALL.md +15 -9
- package/docs/CONFIG.md +168 -35
- package/docs/ERRORS.md +17 -5
- package/docs/FAQ.md +7 -7
- package/docs/FEATURES.md +96 -28
- package/docs/MIGRATION-1.6.0.md +2 -2
- package/docs/OPERATIONS.md +6 -6
- package/docs/PANEL-INTEGRATION.md +6 -0
- package/docs/PRIVACY.md +88 -0
- package/docs/QUICKSTART.md +5 -5
- package/docs/README.en.md +4 -2
- package/docs/RESIDENT-WATCHER.md +1 -1
- package/docs/SESSION-WATCH.md +6 -6
- package/docs/TROUBLESHOOTING.md +4 -4
- package/package.json +47 -117
- package/runbook.md +2 -2
- package/scripts/backfill-auto-cards.mjs +98 -0
- package/scripts/install-surgery-task.ps1 +5 -2
- package/scripts/lib/session-watch-fixture.mjs +1 -1
- package/scripts/longlive-doctor.mjs +6 -0
- package/scripts/migrate-180.mjs +163 -0
- package/scripts/recall-index-build.mjs +128 -0
- package/scripts/session-watch-drill.mjs +1 -1
- package/scripts/surgery-run.cmd +1 -1
- package/scripts/unattended-surgery.ps1 +5 -2
- package/scripts/verify-card-anchors.mjs +138 -0
- package/src/actions/audit.js +125 -0
- package/src/actions/evidence.js +122 -0
- package/src/actions/fs.js +162 -0
- package/src/activity/index.js +760 -0
- package/src/activity/model.js +351 -0
- package/src/activity/store.js +644 -0
- package/src/activity/thinking.js +176 -0
- package/src/adapters/constants.js +11 -0
- package/src/adapters/engram/data/aliases.json +51 -0
- package/src/adapters/engram/data/policy.json +8 -0
- package/src/adapters/engram/data/routing-rules.json +546 -0
- package/src/adapters/engram/index.js +235 -0
- package/src/adapters/registry.js +315 -0
- package/src/answer/audit.js +193 -0
- package/src/answer/evidence.js +30 -0
- package/src/answer/guard.js +1370 -0
- package/src/answer/index.js +1067 -0
- package/src/answer/query.js +111 -0
- package/src/answer/second-hop.mjs +101 -0
- package/src/auto-truncate.js +1 -1
- package/src/config.js +171 -47
- package/src/deps.js +6 -4
- package/src/features.js +182 -6
- package/src/heartbeat.js +156 -23
- package/src/identity/handover.js +52 -1
- package/src/identity/index.js +13 -5
- package/src/identity/inject-position.js +2 -2
- package/src/index.js +97 -7
- package/src/lifecycle/gap-clocks.js +328 -0
- package/src/lifecycle/index.js +269 -0
- package/src/live.js +22 -2
- package/src/memory/activity-capture.js +229 -0
- package/src/memory/anchors.js +194 -0
- package/src/memory/audit-records.js +222 -0
- package/src/memory/auto-cards.js +401 -0
- package/src/memory/capture.js +442 -0
- package/src/memory/card-anchors.js +203 -0
- package/src/memory/conflicts.js +106 -0
- package/src/memory/decay.js +311 -0
- package/src/memory/distill.js +471 -0
- package/src/memory/event-index.js +917 -0
- package/src/memory/evidence-chain.js +295 -0
- package/src/memory/evidence.js +145 -0
- package/src/memory/filter-runner.js +1 -1
- package/src/memory/filter.js +5 -1
- package/src/memory/forget-v2.js +410 -0
- package/src/memory/forget.js +71 -0
- package/src/memory/frames-activation.js +247 -0
- package/src/memory/fts-query.js +174 -0
- package/src/memory/governance.js +604 -0
- package/src/memory/index.js +1 -0
- package/src/memory/l2-gate.js +169 -0
- package/src/memory/lock.js +121 -0
- package/src/memory/migrate.js +139 -0
- package/src/memory/recall-v2.js +223 -0
- package/src/memory/recall.js +65 -2
- package/src/memory/records.js +425 -0
- package/src/memory/retention.js +128 -0
- package/src/memory/review.js +124 -0
- package/src/memory/trust.js +88 -0
- package/src/memory/version-chain.js +264 -0
- package/src/memory/wal.js +137 -0
- package/src/memory/working-frame.js +516 -0
- package/src/metrics/classify.mjs +5 -3
- package/src/metrics/index.mjs +21 -1
- package/src/metrics/prestep-latency.mjs +300 -0
- package/src/metrics/store.mjs +25 -0
- package/src/policy-profile.js +18 -3
- package/src/recall/anaphora.mjs +87 -0
- package/src/recall/cards.mjs +7 -0
- package/src/recall/embedding.mjs +8 -8
- package/src/recall/entities.mjs +112 -0
- package/src/recall/index.js +235 -19
- package/src/recall/inject-order.mjs +10 -8
- package/src/recall/privacy.mjs +105 -16
- package/src/recall/recall-core.mjs +116 -10
- package/src/recall/shared-visibility.js +302 -0
- package/src/recall/time-normalizer.mjs +196 -0
- package/src/role/activation.js +191 -0
- package/src/role/attention.js +318 -0
- package/src/role/base.js +36 -0
- package/src/role/channels.js +423 -0
- package/src/role/index.js +425 -0
- package/src/role/interfaces.js +115 -0
- package/src/role/personality.js +370 -0
- package/src/role/readers.js +151 -0
- package/src/role/relationships.js +477 -0
- package/src/role/store.js +202 -0
- package/src/role/writeprotect.js +373 -0
- package/src/router/aliases.json +176 -25
- package/src/router/background/agent-runner.mjs +1 -1
- package/src/router/background/anchor.mjs +1 -1
- package/src/router/background/audit.mjs +2 -2
- package/src/router/background/budget-ledger.mjs +2 -2
- package/src/router/background/host-sim.mjs +1 -1
- package/src/router/background/memory.mjs +4 -4
- package/src/router/background/queue.mjs +1 -1
- package/src/router/background/retry.mjs +1 -1
- package/src/router/background/router.mjs +1 -1
- package/src/router/background/stream.mjs +1 -1
- package/src/router/background/tool-policy.mjs +1 -1
- package/src/router/design-headings.json +5 -5
- package/src/router/face/config.mjs +1 -1
- package/src/router/face/identity.mjs +3 -3
- package/src/router/judge.mjs +8 -7
- package/src/router/policies/generic.json +21 -4
- package/src/router/route.js +109 -22
- package/src/router/router.mjs +10 -6
- package/src/router/routing-rules.json +260 -445
- package/src/router/run-sim.mjs +8 -8
- package/src/router/runtime.js +23 -12
- package/src/router/service/board.mjs +95 -15
- package/src/router/service/config.mjs +5 -5
- package/src/router/service/context.mjs +1 -1
- package/src/router/service/hint.mjs +14 -0
- package/src/router/service/queue.mjs +1 -1
- package/src/router/service/service.mjs +27 -11
- package/src/router/shadow.mjs +4 -3
- package/src/surgery-recovery.js +2 -2
- package/src/tool-output.js +2 -2
- package/src/tools.js +39 -7
- package/src/truncate-visible.js +2 -2
- package/src/ui/actions.js +1312 -0
- package/src/ui/panel-api.js +367 -0
- package/src/ui/panel-auth.js +251 -0
- package/src/ui/panel-integrity.js +243 -0
- package/src/ui/routes-readonly.js +695 -0
- package/src/ui/routes.js +3 -0
- package/docs/DEV.md +0 -279
- package/docs/PRESENCE-BUS-WIRING.md +0 -491
- package/docs/PRESENCE-BUS.md +0 -694
- package/docs/SURGERY.md +0 -186
- package/scripts/mark-core-cards.mjs +0 -289
- package/scripts/pack.mjs +0 -392
- package/scripts/router-proposals.mjs +0 -386
- package/src/router/fixtures/M1.json +0 -16
- package/src/router/fixtures/M2.json +0 -16
- package/src/router/fixtures/M3.json +0 -20
- package/src/router/fixtures/M4.json +0 -20
- package/src/router/fixtures/M5.json +0 -45
- package/src/router/fixtures/M6.json +0 -53
- package/src/router/fixtures/M7.json +0 -46
- package/src/router/fixtures/S1.json +0 -20
- package/src/router/fixtures/S2.json +0 -23
- package/src/router/fixtures/S3.json +0 -20
- package/src/router/fixtures/S4.json +0 -34
- package/src/router/fixtures/S5.json +0 -43
- package/src/router/fixtures/S6.json +0 -47
- package/src/router/fixtures/T1.json +0 -23
- package/src/router/fixtures/T2.json +0 -16
- package/src/router/fixtures/T3.json +0 -19
- package/src/router/fixtures/T4.json +0 -16
- package/src/router/fixtures/X1.json +0 -17
- package/src/router/fixtures/X2.json +0 -20
- package/src/router/gen-design-headings.mjs +0 -107
- package/src/router/policies/owner-rulings-2026-11-03.json +0 -62
- package/test/auto-recall.test.mjs +0 -474
- package/test/auto-truncate.test.mjs +0 -931
- package/test/autobind.test.mjs +0 -385
- package/test/bus.test.mjs +0 -1572
- package/test/cli-guard.test.mjs +0 -198
- package/test/conclusion.test.mjs +0 -408
- package/test/config-schema.test.mjs +0 -204
- package/test/d6-output.test.mjs +0 -194
- package/test/d6.test.mjs +0 -395
- package/test/deps.test.mjs +0 -342
- package/test/doctor-setup.test.mjs +0 -297
- package/test/embedding-recall.test.mjs +0 -347
- package/test/feature-flags.test.mjs +0 -568
- package/test/filter-shadow.test.mjs +0 -170
- package/test/fixtures/baseline-0.8.0-registration.json +0 -556
- package/test/fixtures/d6-header-tools.json +0 -1
- package/test/fixtures/lock-child.mjs +0 -67
- package/test/fixtures/pre-step-container.mjs +0 -100
- package/test/fixtures/privacy-memory/topics/pii-unmarked.md +0 -9
- package/test/fixtures/privacy-memory/topics/secret-local-only.md +0 -11
- package/test/fixtures/profile-session-longlive-config.json +0 -20
- package/test/fixtures/proposal-cas-child.mjs +0 -66
- package/test/host-engine-status.test.mjs +0 -307
- package/test/hot-truncate.test.mjs +0 -1349
- package/test/identity-card-marker.test.mjs +0 -307
- package/test/identity-router/run.mjs +0 -721
- package/test/integration-pre-step.test.mjs +0 -553
- package/test/live.test.mjs +0 -1020
- package/test/maintenance-schedule.test.mjs +0 -538
- package/test/memory.test.mjs +0 -2828
- package/test/metrics.test.mjs +0 -478
- package/test/orchestrator.test.mjs +0 -266
- package/test/p1_probe.mjs +0 -97
- package/test/packaging.test.mjs +0 -182
- package/test/portability.test.mjs +0 -343
- package/test/rc4-regressions.test.mjs +0 -562
- package/test/rc5-regressions.test.mjs +0 -197
- package/test/rc6-chinese.test.mjs +0 -394
- package/test/router-1100/apply-off.test.mjs +0 -175
- package/test/router-1100/budget-reconcile.test.mjs +0 -215
- package/test/router-1100/config-context.test.mjs +0 -211
- package/test/router-1100/helpers.mjs +0 -129
- package/test/router-1100/hint-envelope.test.mjs +0 -150
- package/test/router-1100/lock-crossproc.test.mjs +0 -206
- package/test/router-1100/proposal-cas-crossproc.test.mjs +0 -184
- package/test/router-1100/router-apply.test.mjs +0 -372
- package/test/router-1100/router-fixb.test.mjs +0 -602
- package/test/router-1100/router-proposals.test.mjs +0 -405
- package/test/router-1100/router-queue-lease-wal.test.mjs +0 -355
- package/test/router-1100/router-security-cr.test.mjs +0 -333
- package/test/router-1100/service-lifecycle.test.mjs +0 -220
- package/test/router-1100/tools-contract.test.mjs +0 -164
- package/test/router-1100/turn-reflect-tick.test.mjs +0 -89
- package/test/router-1100/v6-contract.test.mjs +0 -158
- package/test/router-1100/v6b-contract.test.mjs +0 -293
- package/test/router-1100/v6c-contract.test.mjs +0 -249
- package/test/router-1100/wal-rotation.test.mjs +0 -375
- package/test/router-1100/wp1-config.test.mjs +0 -170
- package/test/router-1100/wp1-default-off.test.mjs +0 -77
- package/test/router-1100/wp1-model-status.test.mjs +0 -90
- package/test/router-1100/wp1-status.test.mjs +0 -220
- package/test/router-1100/wp1-tools.test.mjs +0 -146
- package/test/router-1100/wp5-cli.test.mjs +0 -240
- package/test/router-1100/wp5-model-auto.test.mjs +0 -418
- package/test/router-1100/wp6-merge.test.mjs +0 -386
- package/test/router-1100/wp6-wake.test.mjs +0 -502
- package/test/router-background/audit.test.mjs +0 -37
- package/test/router-background/budget-ledger.test.mjs +0 -330
- package/test/router-background/cross-process.test.mjs +0 -222
- package/test/router-background/fixtures/cross-child.mjs +0 -85
- package/test/router-background/fixtures/real-shapes.mjs +0 -239
- package/test/router-background/router.test.mjs +0 -1044
- package/test/router-background/stream-retry.test.mjs +0 -358
- package/test/router-background/tool-agent.test.mjs +0 -384
- package/test/router-face/bridge.test.mjs +0 -169
- package/test/router-face/face.test.mjs +0 -316
- package/test/router-failclosed.test.mjs +0 -466
- package/test/router-identity.test.mjs +0 -384
- package/test/router-l1-l2.contract.test.mjs +0 -1075
- package/test/router-memory-tools.test.mjs +0 -262
- package/test/router-policy.test.mjs +0 -225
- package/test/router-runtime.test.mjs +0 -438
- package/test/router-shadow.test.mjs +0 -287
- package/test/router.test.mjs +0 -903
- package/test/run-rc.mjs +0 -299
- package/test/run.mjs +0 -2469
- package/test/safety-defaults.test.mjs +0 -179
- package/test/seeded-load.test.mjs +0 -436
- package/test/session-original.test.mjs +0 -338
- package/test/surgery-recovery.test.mjs +0 -936
- package/test/t7_drive.mjs +0 -151
- package/test/tool-output.test.mjs +0 -393
- package/test/tool-schema.test.mjs +0 -177
- package/test/tools_contract.test.mjs +0 -716
- package/test/truncate-visible.test.mjs +0 -1150
- package/test/ui-flags.test.mjs +0 -477
- 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
|
+
}
|
package/src/role/base.js
ADDED
|
@@ -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
|
+
}
|