@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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* auto-recall v0 — 「自动想起」的纯函数核心。
|
|
3
3
|
*
|
|
4
4
|
* 设计约束(见 ../../DESIGN.md):
|
|
5
|
-
* -
|
|
5
|
+
* - 不 import fs/网络/宿主 API,可直接单测;唯一依赖是纯函数 trust 公式(A3 排序权重);
|
|
6
6
|
* - 打分算法与 scope-route 原型对齐(词袋子串命中 + 长度权重),
|
|
7
7
|
* 以便沿用它在金标 26/27、留出集 9/10 上的既有结论;
|
|
8
8
|
* - 没命中 / 低于阈值 / 超预算 → 不注入(items: []),绝不「猜一个最像的」;
|
|
@@ -18,6 +18,10 @@
|
|
|
18
18
|
* defaultPrivacyScan(text) -> {verdict, spans, ...} // 无外部依赖的 PII 兜底
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
|
+
import { trustScoreAt } from '../memory/trust.js';
|
|
22
|
+
import { cardTimeAffinity } from './time-normalizer.mjs';
|
|
23
|
+
import { resolveEntityAliases } from './entities.mjs';
|
|
24
|
+
|
|
21
25
|
export const VERSION = 'auto-recall/0.1.0';
|
|
22
26
|
|
|
23
27
|
export const DEFAULTS = Object.freeze({
|
|
@@ -38,6 +42,29 @@ export const DEFAULTS = Object.freeze({
|
|
|
38
42
|
/** 隐私命中且 privacyPolicy='title' 时,正文位置显示这个标记。 */
|
|
39
43
|
export const PRIVACY_HIDDEN_MARK = '[已隐藏]';
|
|
40
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Render card-controlled strings as inert data: no newlines/control chars, no
|
|
47
|
+
* bidi overrides, collapsed whitespace, bounded length. The template around it
|
|
48
|
+
* is fixed; this function is the only path for title/body/path text.
|
|
49
|
+
*/
|
|
50
|
+
export function sanitizeDataText(value, max = 400) {
|
|
51
|
+
const text = String(value ?? '')
|
|
52
|
+
.replace(/[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2060-\u206f\uFEFF]/gu, ' ')
|
|
53
|
+
.replace(/\s+/gu, ' ')
|
|
54
|
+
.trim();
|
|
55
|
+
return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}…` : text;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Safe anchor origin line: generated only from validated anchor fields. */
|
|
59
|
+
export function renderAnchorDataLine(anchor) {
|
|
60
|
+
if (!anchor || typeof anchor.sessionId !== 'string' || !Number.isInteger(anchor.turn) || anchor.turn < 0) return '';
|
|
61
|
+
const suffix = `#${anchor.turn}]`;
|
|
62
|
+
const prefix = '[原文 ';
|
|
63
|
+
const maxSession = Math.max(1, 60 - prefix.length - suffix.length);
|
|
64
|
+
const session = anchor.sessionId.replace(/[^A-Za-z0-9._:-]/gu, '_').slice(0, maxSession);
|
|
65
|
+
return `${prefix}${session}${suffix}`;
|
|
66
|
+
}
|
|
67
|
+
|
|
41
68
|
/**
|
|
42
69
|
* 关键词 + 向量融合(A4)的默认权重。
|
|
43
70
|
*
|
|
@@ -49,6 +76,40 @@ export const PRIVACY_HIDDEN_MARK = '[已隐藏]';
|
|
|
49
76
|
* semanticAdmit 是「零关键词命中」卡的余弦准入线:本地 bge-small-zh 在无关
|
|
50
77
|
* 静默查询上的最高余弦实测 0.498,因此 0.55 能挡住沉默样本,又放得进真改写。
|
|
51
78
|
*/
|
|
79
|
+
/** A3 ranking floors: trust 0 => half score; trust 1 => full score; never filters a card out. */
|
|
80
|
+
export const TRUST_RANKING_FLOOR = 0.5;
|
|
81
|
+
|
|
82
|
+
function clamp01(value) {
|
|
83
|
+
if (!Number.isFinite(value)) return 0;
|
|
84
|
+
return Math.max(0, Math.min(1, value));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Neutral (1.0) unless the card carries a trust hint; no trust hint => legacy ordering. */
|
|
88
|
+
export function trustFactorOf(card, { trustNow = null } = {}) {
|
|
89
|
+
if (!card || typeof card !== 'object') return { factor: 1, trustScore: null, lowTrust: false };
|
|
90
|
+
const hasHint = Number.isFinite(card.trustScore) || typeof card.sourceTier === 'string';
|
|
91
|
+
if (!hasHint) return { factor: 1, trustScore: null, lowTrust: false };
|
|
92
|
+
let score = Number.isFinite(card.trustScore) ? clamp01(card.trustScore) : null;
|
|
93
|
+
if (score === null) {
|
|
94
|
+
// Event-time decay: caller injects the turn/event wall clock; createdAt comes from the card
|
|
95
|
+
// (date/lastUsed). No Date.now() in ranking => deterministic tests and no wall-clock games.
|
|
96
|
+
const created = card.createdAt ?? card.date ?? card.lastUsed ?? null;
|
|
97
|
+
const { trustScore } = trustScoreAt({
|
|
98
|
+
sourceTier: card.sourceTier,
|
|
99
|
+
verifyCount: Number.isInteger(card.verifyCount) ? card.verifyCount : 0,
|
|
100
|
+
createdAt: created,
|
|
101
|
+
now: trustNow ?? null,
|
|
102
|
+
legacy: false,
|
|
103
|
+
});
|
|
104
|
+
score = trustScore;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
factor: TRUST_RANKING_FLOOR + (1 - TRUST_RANKING_FLOOR) * score,
|
|
108
|
+
trustScore: score,
|
|
109
|
+
lowTrust: score < 0.35,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
52
113
|
export const FUSION_DEFAULTS = Object.freeze({
|
|
53
114
|
keywordWeight: 0.6,
|
|
54
115
|
vectorWeight: 0.4,
|
|
@@ -296,7 +357,7 @@ export function pickExcerpt(card, hits, maxTokens) {
|
|
|
296
357
|
}
|
|
297
358
|
|
|
298
359
|
/* ------------------------------------------------------------------ *
|
|
299
|
-
* 3. 去重(与已注入的 handover 卡 /
|
|
360
|
+
* 3. 去重(与已注入的 handover 卡 / 外部记忆事实比字面)
|
|
300
361
|
* ------------------------------------------------------------------ */
|
|
301
362
|
|
|
302
363
|
function bigrams(text) {
|
|
@@ -366,7 +427,7 @@ export function maxCosine(queryVector, vectorList) {
|
|
|
366
427
|
* localOnly?:boolean}>} input.cards
|
|
367
428
|
* @param {number} [input.budget] 本次注入的 token 预算
|
|
368
429
|
* @param {string[]} [input.seenPaths] 本会话已注入过的卡路径(不再重复)
|
|
369
|
-
* @param {string[]} [input.injectedTexts] 本会话已注入的其它文本(handover /
|
|
430
|
+
* @param {string[]} [input.injectedTexts] 本会话已注入的其它文本(handover / 外部记忆事实)
|
|
370
431
|
* @param {object} [input.options] 覆盖 DEFAULTS 的阈值
|
|
371
432
|
* @param {(text:string)=>{verdict:string}|boolean} [input.options.privacyScan]
|
|
372
433
|
* 注入前的隐私扫描器;不传则用 defaultPrivacyScan(保守 PII 兜底)。
|
|
@@ -398,6 +459,12 @@ export function chooseRecall({
|
|
|
398
459
|
&& vectors,
|
|
399
460
|
);
|
|
400
461
|
const fusion = { ...FUSION_DEFAULTS, ...(opt.fusion || {}) };
|
|
462
|
+
// B2 context channels (default neutral: no inputs => zero bonus, legacy ordering unchanged).
|
|
463
|
+
const queryTimeRefs = Array.isArray(opt.queryTimeRefs) ? opt.queryTimeRefs : [];
|
|
464
|
+
const entityAliases = opt.entityAliases && typeof opt.entityAliases === 'object' && !Array.isArray(opt.entityAliases)
|
|
465
|
+
? opt.entityAliases
|
|
466
|
+
: null;
|
|
467
|
+
const entityHits = entityAliases ? resolveEntityAliases(queryText, entityAliases).matches : [];
|
|
401
468
|
const scan = typeof opt.privacyScan === 'function' ? opt.privacyScan : defaultPrivacyScan;
|
|
402
469
|
const stats = {
|
|
403
470
|
eligible: 0,
|
|
@@ -412,6 +479,8 @@ export function chooseRecall({
|
|
|
412
479
|
redactedByPrivacy: 0,
|
|
413
480
|
semanticCandidates: 0,
|
|
414
481
|
semanticAdmitted: 0,
|
|
482
|
+
droppedByLowTrust: 0,
|
|
483
|
+
...(queryTimeRefs.length > 0 || entityAliases ? { contextBoosted: 0 } : {}),
|
|
415
484
|
};
|
|
416
485
|
|
|
417
486
|
if (typeof queryText !== 'string' || queryText.trim() === '' || !Array.isArray(cards) || cards.length === 0) {
|
|
@@ -432,6 +501,7 @@ export function chooseRecall({
|
|
|
432
501
|
// 关键词命中仍走原来的 minScore/minHits 门槛;零命中卡只有在余弦
|
|
433
502
|
// ≥ semanticAdmit 且融合分 ≥ minFusedScore 时才准进来(语义召回的唯一入口)。
|
|
434
503
|
const eligible = [];
|
|
504
|
+
let trustRanked = 0;
|
|
435
505
|
for (const c of scored) {
|
|
436
506
|
const keywordEligible = c.hitCount >= opt.minHits && c.score >= opt.minScore;
|
|
437
507
|
const semanticEligible = fuse && Number.isFinite(c.cos) && c.cos >= fusion.semanticAdmit;
|
|
@@ -450,9 +520,35 @@ export function chooseRecall({
|
|
|
450
520
|
stats.semanticCandidates += 1;
|
|
451
521
|
stats.semanticAdmitted += 1;
|
|
452
522
|
}
|
|
453
|
-
|
|
523
|
+
// B2: time affinity (card date in a normalized query time range) + entity alias affinity.
|
|
524
|
+
let contextBonus = 0;
|
|
525
|
+
if (queryTimeRefs.length > 0 && cardTimeAffinity(c.card, queryTimeRefs) > 0) contextBonus += 0.15;
|
|
526
|
+
if (entityHits.length > 0) {
|
|
527
|
+
const cardText = `${c.card?.title ?? ''}\n${c.card?.body ?? ''}`;
|
|
528
|
+
if (entityHits.some((hit) => cardText.includes(hit.canonical) || cardText.includes(hit.alias))) contextBonus += 0.1;
|
|
529
|
+
}
|
|
530
|
+
if (contextBonus > 0 && Object.prototype.hasOwnProperty.call(stats, 'contextBoosted')) stats.contextBoosted += 1;
|
|
531
|
+
const trust = trustFactorOf(c.card, { trustNow: opt.trustNow ?? null });
|
|
532
|
+
if (trust.trustScore !== null) trustRanked += 1;
|
|
533
|
+
// A3 policy: default 'downrank' (data preserved); 'exclude' drops only from injection.
|
|
534
|
+
if (opt.trustPolicy === 'exclude' && trust.lowTrust === true) { stats.droppedByLowTrust += 1; continue; }
|
|
535
|
+
eligible.push({
|
|
536
|
+
...c,
|
|
537
|
+
fused,
|
|
538
|
+
reason: fuse ? (c.hitCount > 0 ? 'keyword+vector' : 'vector') : 'keyword',
|
|
539
|
+
trustFactor: trust.factor,
|
|
540
|
+
trustScore: trust.trustScore,
|
|
541
|
+
lowTrust: trust.lowTrust,
|
|
542
|
+
contextBonus,
|
|
543
|
+
rankScore: Math.round((fused * trust.factor + contextBonus) * 1000) / 1000,
|
|
544
|
+
});
|
|
454
545
|
}
|
|
455
|
-
|
|
546
|
+
// A3: low-trust cards are downranked by score weight, not dropped.
|
|
547
|
+
if (trustRanked > 0) stats.downrankedLowTrust = 0;
|
|
548
|
+
const isEventCard = (cand) => String(cand?.path ?? '').startsWith('auto:');
|
|
549
|
+
eligible.sort((a, b) => (Number(isEventCard(a)) - Number(isEventCard(b)))
|
|
550
|
+
|| b.rankScore - a.rankScore || b.fused - a.fused || b.score - a.score || b.hitCount - a.hitCount || a.path.localeCompare(b.path));
|
|
551
|
+
if (trustRanked > 0) stats.downrankedLowTrust = eligible.filter((item) => item.lowTrust === true).length;
|
|
456
552
|
|
|
457
553
|
stats.eligible = eligible.length;
|
|
458
554
|
stats.best = eligible.length > 0 ? { path: eligible[0].path, score: eligible[0].fused } : null;
|
|
@@ -508,6 +604,8 @@ export function chooseRecall({
|
|
|
508
604
|
reason: cand.reason,
|
|
509
605
|
excerpt: PRIVACY_HIDDEN_MARK,
|
|
510
606
|
privacyRedacted: true,
|
|
607
|
+
...(cand.cardId ? { cardId: cand.cardId } : {}),
|
|
608
|
+
...(cand.anchor ? { anchor: cand.anchor } : {}),
|
|
511
609
|
_hits: [],
|
|
512
610
|
});
|
|
513
611
|
remaining -= hiddenCost;
|
|
@@ -519,7 +617,7 @@ export function chooseRecall({
|
|
|
519
617
|
continue;
|
|
520
618
|
}
|
|
521
619
|
|
|
522
|
-
// 去重:与已注入的 handover /
|
|
620
|
+
// 去重:与已注入的 handover / 外部记忆事实比字面包含度
|
|
523
621
|
if (injectedTexts.length > 0) {
|
|
524
622
|
const dup = injectedTexts.some(
|
|
525
623
|
(t) => typeof t === 'string' && t.trim() !== '' && containment(excerpt, t) >= opt.dedupeContainment,
|
|
@@ -536,6 +634,8 @@ export function chooseRecall({
|
|
|
536
634
|
score: cand.fused,
|
|
537
635
|
reason: cand.reason,
|
|
538
636
|
excerpt,
|
|
637
|
+
...(cand.cardId ? { cardId: cand.cardId } : {}),
|
|
638
|
+
...(cand.anchor ? { anchor: cand.anchor } : {}),
|
|
539
639
|
_hits: cand.hits.map((h) => h.phrase),
|
|
540
640
|
});
|
|
541
641
|
remaining -= cost;
|
|
@@ -566,10 +666,10 @@ export function renderItemHeader(item, { showScore = true, withPath = true, priv
|
|
|
566
666
|
const meta = [];
|
|
567
667
|
if (withPath) {
|
|
568
668
|
const scan = typeof privacyScan === 'function' ? privacyScan : defaultPrivacyScan;
|
|
569
|
-
meta.push(privacyBlocked(scan, item.path) ? PRIVACY_HIDDEN_PATH : item.path);
|
|
669
|
+
meta.push(privacyBlocked(scan, item.path) ? PRIVACY_HIDDEN_PATH : sanitizeDataText(item.path, 200));
|
|
570
670
|
}
|
|
571
671
|
if (showScore) meta.push(`相关度 ${Number(item.score).toFixed(2)}`);
|
|
572
|
-
return `- 《${item.title}》(${meta.join(' · ')})`;
|
|
672
|
+
return `- 《${sanitizeDataText(item.title, 160)}》(${meta.join(' · ')})`;
|
|
573
673
|
}
|
|
574
674
|
|
|
575
675
|
/* ------------------------------------------------------------------ *
|
|
@@ -587,8 +687,14 @@ export function renderInjection(result, opts = {}) {
|
|
|
587
687
|
const lines = [header];
|
|
588
688
|
for (const item of items) {
|
|
589
689
|
lines.push(renderItemHeader(item, opts));
|
|
590
|
-
|
|
591
|
-
|
|
690
|
+
// Card body is data: one inert line, never multiple model-readable lines.
|
|
691
|
+
const body = sanitizeDataText(item.excerpt || '', 400);
|
|
692
|
+
if (body !== '') lines.push(` ${body}`);
|
|
693
|
+
// 1.8.0 AutoCard: origin line is generated only from the validated anchor.
|
|
694
|
+
if (item && item.anchor) {
|
|
695
|
+
const origin = renderAnchorDataLine(item.anchor);
|
|
696
|
+
if (origin !== '') lines.push(` ${origin}`);
|
|
697
|
+
}
|
|
592
698
|
}
|
|
593
699
|
return lines.join('\n');
|
|
594
700
|
}
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
// src/recall/shared-visibility.js — M2-C R1–R6:双我可见的共享可见块
|
|
2
|
+
//
|
|
3
|
+
// 新会话首轮 agent/pre-step 注入一块 ≤300 字的「共享可见块」(session-reference
|
|
4
|
+
// 隐藏消息),让当前会话看到**其它活体会话**的目标 / 末活动 / 最后真实用户消息 /
|
|
5
|
+
// 最后记忆写入 / 陈旧标记,无需读原始日志(R1:≤2s;R3:不拿心跳冒充实时)。
|
|
6
|
+
//
|
|
7
|
+
// 数据源:
|
|
8
|
+
// - 活体表:liveList(与面板 #23 frames 同口径,src/live.js)
|
|
9
|
+
// - 记忆写入:resolveMemoryWriteAt(与 memory-gap 同源,src/lifecycle/gap-clocks.js)
|
|
10
|
+
// - 最后真实用户消息:事件索引(.longlive/index/events.db,kind=user),
|
|
11
|
+
// 无索引时退回 live 行扩展字段;两者都没有 => 不猜(lastUserMessage=null)
|
|
12
|
+
//
|
|
13
|
+
// 纪律:只读;失败静默(不阻断 turn);隐私门命中整行丢弃;≤300 字;只首轮注入。
|
|
14
|
+
|
|
15
|
+
import { liveList } from '../live.js';
|
|
16
|
+
import { resolveMemoryWriteAt } from '../lifecycle/gap-clocks.js';
|
|
17
|
+
|
|
18
|
+
export const SHARED_VISIBILITY_SCHEMA = 'shared-visibility/v1';
|
|
19
|
+
export const SHARED_BLOCK_MAX_CHARS = 300;
|
|
20
|
+
export const GOAL_STALE_MS = 5 * 60 * 1000;
|
|
21
|
+
export const SESSION_STALE_MS = 30 * 60 * 1000;
|
|
22
|
+
export const ACK_WORDS = Object.freeze(['对', '好', '嗯', '收到', '可以', '是的', '行', '不错', '好的', '知道了', 'ok', 'OK']);
|
|
23
|
+
const ACK_RE = new RegExp(`^(?:${ACK_WORDS.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|')})[。.!!??~~ ]*$`, 'u');
|
|
24
|
+
|
|
25
|
+
function toMs(value) {
|
|
26
|
+
if (value instanceof Date) return value.getTime();
|
|
27
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
28
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
29
|
+
const parsed = Date.parse(value);
|
|
30
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function clampText(value, max) {
|
|
36
|
+
const text = String(value ?? '').replace(/\s+/gu, ' ').trim();
|
|
37
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** 「无信息量」判定:长度 <6 或命中 ack 词表(「对的」不是最后意图)。 */
|
|
41
|
+
export function isInformativeUserText(text) {
|
|
42
|
+
const value = String(text ?? '').trim();
|
|
43
|
+
if (value === '') return false;
|
|
44
|
+
if ([...value].length < 6) return false;
|
|
45
|
+
return !ACK_RE.test(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function isoWithOffset(ms, offsetMinutes = 480) {
|
|
49
|
+
if (!Number.isFinite(ms)) return null;
|
|
50
|
+
const shifted = new Date(ms + offsetMinutes * 60 * 1000);
|
|
51
|
+
const pad = (n, w = 2) => String(n).padStart(w, '0');
|
|
52
|
+
return `${shifted.getUTCFullYear()}-${pad(shifted.getUTCMonth() + 1)}-${pad(shifted.getUTCDate())}T${pad(shifted.getUTCHours())}:${pad(shifted.getUTCMinutes())}:${pad(shifted.getUTCSeconds())}.${pad(shifted.getUTCMilliseconds(), 3)}+08:00`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function clockText(ms, offsetMinutes = 480) {
|
|
56
|
+
if (!Number.isFinite(ms)) return '';
|
|
57
|
+
const shifted = new Date(ms + offsetMinutes * 60 * 1000);
|
|
58
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
59
|
+
return `${pad(shifted.getUTCHours())}:${pad(shifted.getUTCMinutes())}:${pad(shifted.getUTCSeconds())}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function ageText(ms) {
|
|
63
|
+
if (!Number.isFinite(ms) || ms < 0) return '';
|
|
64
|
+
const minutes = Math.floor(ms / 60_000);
|
|
65
|
+
if (minutes < 1) return '刚刚';
|
|
66
|
+
if (minutes < 60) return `${minutes} 分钟前`;
|
|
67
|
+
const hours = Math.floor(minutes / 60);
|
|
68
|
+
if (hours < 24) return `${hours} 小时前`;
|
|
69
|
+
return `${Math.floor(hours / 24)} 天前`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function pickLastUserMessage(sessionId, lastUserMessages) {
|
|
73
|
+
const entry = lastUserMessages?.[sessionId] ?? null;
|
|
74
|
+
if (!entry || typeof entry !== 'object') return null;
|
|
75
|
+
if (typeof entry.text !== 'string' || entry.text.trim() === '') return null;
|
|
76
|
+
const text = clampText(entry.text, 80);
|
|
77
|
+
const prev = Array.isArray(entry.prev)
|
|
78
|
+
? entry.prev.filter((item) => item && typeof item.text === 'string' && item.text.trim() !== '').slice(0, 2).map((item) => ({ text: clampText(item.text, 60), at: item.at ?? null }))
|
|
79
|
+
: [];
|
|
80
|
+
return { text, at: entry.at ?? null, informative: isInformativeUserText(text), prev };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Pure core: build the shared-visibility rows from live entries + injected data.
|
|
85
|
+
* @returns {{schema,sessions:object[],injectedAt:string,count:number}|null}
|
|
86
|
+
*/
|
|
87
|
+
export function buildSharedVisibility({
|
|
88
|
+
entries = [],
|
|
89
|
+
currentSessionId = null,
|
|
90
|
+
now = Date.now(),
|
|
91
|
+
memoryWrite = null,
|
|
92
|
+
lastUserMessages = {},
|
|
93
|
+
projectOf = null,
|
|
94
|
+
maxSessions = 2,
|
|
95
|
+
} = {}) {
|
|
96
|
+
const nowMs = toMs(now) ?? Date.now();
|
|
97
|
+
const sessions = [];
|
|
98
|
+
for (const entry of Array.isArray(entries) ? entries : []) {
|
|
99
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
100
|
+
if (entry.sessionId === currentSessionId) continue;
|
|
101
|
+
if (entry.effectiveState === 'dead' || entry.effectiveState === 'corrupt') continue;
|
|
102
|
+
const updatedMs = toMs(entry.updatedAt);
|
|
103
|
+
const goalAtMs = toMs(entry.goalUpdatedAt) ?? updatedMs;
|
|
104
|
+
const goalText = clampText(entry.goal, 80);
|
|
105
|
+
const action = entry.last_action && typeof entry.last_action === 'object' ? entry.last_action : null;
|
|
106
|
+
const actionAtMs = toMs(action?.at);
|
|
107
|
+
const attribution = {
|
|
108
|
+
present: entry.sessionId != null,
|
|
109
|
+
// 归属校验 = 行里同时有 host+pid 证据;不拿心跳冒充实时(R3)。
|
|
110
|
+
verified: typeof entry.host === 'string' && entry.host.trim() !== '' && Number.isInteger(entry.pid),
|
|
111
|
+
};
|
|
112
|
+
sessions.push({
|
|
113
|
+
project: typeof projectOf === 'function' ? (projectOf(entry) ?? null) : (entry.project ?? null),
|
|
114
|
+
sessionId: entry.sessionId,
|
|
115
|
+
attribution,
|
|
116
|
+
goal: goalText === '' ? null : {
|
|
117
|
+
text: goalText,
|
|
118
|
+
ageMs: Number.isFinite(goalAtMs) ? Math.max(0, nowMs - goalAtMs) : null,
|
|
119
|
+
stale: !Number.isFinite(goalAtMs) || nowMs - goalAtMs > GOAL_STALE_MS,
|
|
120
|
+
},
|
|
121
|
+
lastAction: action === null ? null : {
|
|
122
|
+
kind: clampText(action.kind ?? action.type ?? 'action', 24),
|
|
123
|
+
detail: clampText(action.detail ?? action.summary ?? '', 120),
|
|
124
|
+
at: isoWithOffset(actionAtMs),
|
|
125
|
+
ageMs: Number.isFinite(actionAtMs) ? Math.max(0, nowMs - actionAtMs) : null,
|
|
126
|
+
stale: !Number.isFinite(actionAtMs) || nowMs - actionAtMs > GOAL_STALE_MS,
|
|
127
|
+
},
|
|
128
|
+
lastUserMessage: pickLastUserMessage(entry.sessionId, lastUserMessages),
|
|
129
|
+
lastMemoryWrite: memoryWrite && Number.isFinite(toMs(memoryWrite.atMs))
|
|
130
|
+
? {
|
|
131
|
+
at: isoWithOffset(toMs(memoryWrite.atMs)),
|
|
132
|
+
sourceFile: memoryWrite.rel ?? memoryWrite.sourceFile ?? null,
|
|
133
|
+
gapSec: Number.isFinite(memoryWrite.gapSec) ? memoryWrite.gapSec : (Number.isFinite(toMs(memoryWrite.atMs)) ? Math.max(0, Math.floor((nowMs - toMs(memoryWrite.atMs)) / 1000)) : null),
|
|
134
|
+
}
|
|
135
|
+
: null,
|
|
136
|
+
injectedAt: isoWithOffset(nowMs),
|
|
137
|
+
stale: !Number.isFinite(updatedMs) || nowMs - updatedMs > SESSION_STALE_MS,
|
|
138
|
+
effectiveState: entry.effectiveState ?? entry.state ?? null,
|
|
139
|
+
});
|
|
140
|
+
if (sessions.length >= Math.max(1, Number(maxSessions) || 2)) break;
|
|
141
|
+
}
|
|
142
|
+
if (sessions.length === 0) return null;
|
|
143
|
+
return { schema: SHARED_VISIBILITY_SCHEMA, sessions, injectedAt: isoWithOffset(nowMs), count: sessions.length };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Render the ≤300-char hidden block. Privacy hits drop the whole line (never the turn). */
|
|
147
|
+
export function renderSharedVisibilityBlock(visibility, { maxChars = SHARED_BLOCK_MAX_CHARS, privacyScan = null } = {}) {
|
|
148
|
+
if (!visibility || !Array.isArray(visibility.sessions) || visibility.sessions.length === 0) return '';
|
|
149
|
+
const scan = typeof privacyScan === 'function' ? privacyScan : null;
|
|
150
|
+
const safe = (text) => {
|
|
151
|
+
if (typeof text !== 'string' || text === '') return '';
|
|
152
|
+
if (scan !== null) {
|
|
153
|
+
try {
|
|
154
|
+
if (scan(text)?.verdict === 'local_only') return '';
|
|
155
|
+
} catch { return ''; }
|
|
156
|
+
}
|
|
157
|
+
return text;
|
|
158
|
+
};
|
|
159
|
+
const lines = [`【共享可见 · 只读】现在 ${visibility.count} 个思维活着。`];
|
|
160
|
+
let memoryWriteLine = '';
|
|
161
|
+
for (const session of visibility.sessions) {
|
|
162
|
+
const shortId = session.sessionId ? session.sessionId.slice(0, 18) : 'unknown';
|
|
163
|
+
const project = session.project ? `(${session.project})` : '';
|
|
164
|
+
const goal = session.goal
|
|
165
|
+
? `目标:${safe(session.goal.text)}${session.goal.stale ? `(${ageText(session.goal.ageMs)}·陈旧)` : ''}`
|
|
166
|
+
: '';
|
|
167
|
+
const action = session.lastAction
|
|
168
|
+
? `末活动:${safe(session.lastAction.kind)}/${safe(session.lastAction.detail)}${Number.isFinite(session.lastAction.ageMs) ? `(${ageText(session.lastAction.ageMs)})` : ''}`
|
|
169
|
+
: '';
|
|
170
|
+
const user = session.lastUserMessage
|
|
171
|
+
? `最后用户消息:「${safe(session.lastUserMessage.text)}」${session.lastUserMessage.informative ? '' : '(无信息量)'}${session.lastUserMessage.prev.length > 0 ? `;前一条:「${safe(session.lastUserMessage.prev[0].text)}」` : ''}`
|
|
172
|
+
: '';
|
|
173
|
+
const row = [shortId + project, goal, action, user].filter(Boolean).join('|');
|
|
174
|
+
if (row !== '') lines.push(`· ${row}`);
|
|
175
|
+
if (session.lastMemoryWrite && session.lastMemoryWrite.sourceFile) {
|
|
176
|
+
memoryWriteLine = `· 最后记忆写入 ${clockText(toMs(session.lastMemoryWrite.at))} · 来源 ${safe(String(session.lastMemoryWrite.sourceFile))}`;
|
|
177
|
+
}
|
|
178
|
+
if (session.stale && session.lastAction === null) lines.push('· (该会话已 >30 分钟无心跳,按陈旧处理)');
|
|
179
|
+
}
|
|
180
|
+
if (memoryWriteLine !== '') lines.push(memoryWriteLine);
|
|
181
|
+
let text = lines.join('\n');
|
|
182
|
+
if (text.length > maxChars && memoryWriteLine !== '') {
|
|
183
|
+
// R1: the memory-write line is contract data (golden) — keep it even when
|
|
184
|
+
// trimming the rest of the block to the ≤300-char budget.
|
|
185
|
+
const header = lines[0];
|
|
186
|
+
const bodyLines = lines.slice(1).filter((line) => line !== memoryWriteLine);
|
|
187
|
+
let budget = maxChars - header.length - memoryWriteLine.length - 2;
|
|
188
|
+
if (budget < 0) {
|
|
189
|
+
text = `${header}\n${memoryWriteLine}`.slice(0, Math.max(0, maxChars - 1)) + '…';
|
|
190
|
+
return text;
|
|
191
|
+
}
|
|
192
|
+
const kept = [];
|
|
193
|
+
for (const line of bodyLines) {
|
|
194
|
+
if (line.length <= budget) {
|
|
195
|
+
kept.push(line);
|
|
196
|
+
budget -= line.length + 1;
|
|
197
|
+
} else {
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
text = [header, ...kept, memoryWriteLine].join('\n');
|
|
202
|
+
}
|
|
203
|
+
if (text.length > maxChars) text = `${text.slice(0, Math.max(0, maxChars - 1))}…`;
|
|
204
|
+
return text;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function readIndexLastUserMessages({ memoryRoot, sessionIds = [] } = {}) {
|
|
208
|
+
const wanted = new Set(Array.isArray(sessionIds) ? sessionIds : []);
|
|
209
|
+
if (wanted.size === 0) return {};
|
|
210
|
+
try {
|
|
211
|
+
// Lazy import keeps the module cheap when no index exists.
|
|
212
|
+
const eventIndex = requireEventIndex();
|
|
213
|
+
const dbPath = `${String(memoryRoot)}/.longlive/index/events.db`;
|
|
214
|
+
if (!eventIndex || !dbPath) return {};
|
|
215
|
+
const fs = requireFs();
|
|
216
|
+
const path = requirePath();
|
|
217
|
+
const resolved = path.resolve(String(memoryRoot));
|
|
218
|
+
if (!fs.existsSync(path.join(resolved, '.longlive', 'index', 'events.db'))) return {};
|
|
219
|
+
const index = eventIndex.openEventIndex({ memoryRoot: resolved });
|
|
220
|
+
try {
|
|
221
|
+
const rows = index.listEvents({ kind: 'user', limit: 200 });
|
|
222
|
+
const out = {};
|
|
223
|
+
for (const sessionId of wanted) {
|
|
224
|
+
const own = rows.filter((row) => row.session_id === sessionId)
|
|
225
|
+
.sort((a, b) => (toMs(b.time_end) ?? 0) - (toMs(a.time_end) ?? 0));
|
|
226
|
+
if (own.length === 0) continue;
|
|
227
|
+
const text = String(own[0].summary ?? '').trim();
|
|
228
|
+
if (text === '') continue;
|
|
229
|
+
out[sessionId] = {
|
|
230
|
+
text,
|
|
231
|
+
at: isoWithOffset(toMs(own[0].time_end)),
|
|
232
|
+
prev: own.slice(1, 3).map((row) => ({ text: String(row.summary ?? '').trim(), at: isoWithOffset(toMs(row.time_end)) })),
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
} finally {
|
|
237
|
+
index.close();
|
|
238
|
+
}
|
|
239
|
+
} catch {
|
|
240
|
+
return {};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Node builtins are loaded lazily so the pure builders stay dependency-free at import time.
|
|
245
|
+
let cachedEventIndex = null;
|
|
246
|
+
let cachedFs = null;
|
|
247
|
+
let cachedPath = null;
|
|
248
|
+
function requireEventIndex() {
|
|
249
|
+
return cachedEventIndex;
|
|
250
|
+
}
|
|
251
|
+
function requireFs() {
|
|
252
|
+
return cachedFs;
|
|
253
|
+
}
|
|
254
|
+
function requirePath() {
|
|
255
|
+
return cachedPath;
|
|
256
|
+
}
|
|
257
|
+
async function loadDeps() {
|
|
258
|
+
try {
|
|
259
|
+
cachedEventIndex = await import('../memory/event-index.js');
|
|
260
|
+
cachedFs = await import('node:fs');
|
|
261
|
+
cachedPath = await import('node:path');
|
|
262
|
+
} catch {
|
|
263
|
+
cachedEventIndex = null;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Production wrapper (read-only). Missing pieces degrade to null, never throw.
|
|
269
|
+
* @returns {null|{schema,sessions,injectedAt,count,block}}
|
|
270
|
+
*/
|
|
271
|
+
export function sharedVisibility({ memoryRoot, currentSessionId = null, now = Date.now(), privacyScan = null, projectOf = null } = {}) {
|
|
272
|
+
try {
|
|
273
|
+
loadDepsSync();
|
|
274
|
+
const nowMs = toMs(now) ?? Date.now();
|
|
275
|
+
const listed = liveList({ includeDead: false }, { memoryRoot, now: nowMs });
|
|
276
|
+
const entries = Array.isArray(listed?.entries) ? listed.entries : [];
|
|
277
|
+
if (entries.length === 0) return null;
|
|
278
|
+
let memoryWrite = null;
|
|
279
|
+
try {
|
|
280
|
+
const resolved = resolveMemoryWriteAt({ memoryRoot, bootAtMs: nowMs });
|
|
281
|
+
if (resolved && Number.isFinite(resolved.atMs)) memoryWrite = { atMs: resolved.atMs, rel: resolved.rel };
|
|
282
|
+
} catch { /* memory write is optional */ }
|
|
283
|
+
const sessionIds = entries.map((entry) => entry.sessionId).filter((id) => id != null && id !== currentSessionId);
|
|
284
|
+
const lastUserMessages = readIndexLastUserMessages({ memoryRoot, sessionIds });
|
|
285
|
+
const visibility = buildSharedVisibility({ entries, currentSessionId, now: nowMs, memoryWrite, lastUserMessages, projectOf });
|
|
286
|
+
if (visibility === null) return null;
|
|
287
|
+
const block = renderSharedVisibilityBlock(visibility, { privacyScan });
|
|
288
|
+
return block === '' ? null : { ...visibility, block };
|
|
289
|
+
} catch {
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Resolve lazy deps synchronously via createRequire (fs/path) and a static import of the index.
|
|
295
|
+
import fsDefault from 'node:fs';
|
|
296
|
+
import pathDefault from 'node:path';
|
|
297
|
+
import * as eventIndexModule from '../memory/event-index.js';
|
|
298
|
+
function loadDepsSync() {
|
|
299
|
+
cachedEventIndex = eventIndexModule;
|
|
300
|
+
cachedFs = fsDefault;
|
|
301
|
+
cachedPath = pathDefault;
|
|
302
|
+
}
|