@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,401 @@
|
|
|
1
|
+
// src/memory/cards.js — 1.8.0 AutoCard: EventRecord → append-only index card.
|
|
2
|
+
//
|
|
3
|
+
// Minimal deterministic distillation: no LLM, no network, no mutation of the
|
|
4
|
+
// event index. One card per turn EventRecord, keyed by eventId; repeated
|
|
5
|
+
// captureTurn calls never append a second line for the same eventId.
|
|
6
|
+
//
|
|
7
|
+
// Card shape (spec):
|
|
8
|
+
// { schema, cardId, eventId, sessionId, turn, at, kind, title, terms,
|
|
9
|
+
// anchor: { sessionId, turn, seqStart, seqEnd, digest } }
|
|
10
|
+
//
|
|
11
|
+
// anchor is a copied projection of EventRecord.anchors[0] (the session_turn
|
|
12
|
+
// anchor), so a card can always be verified back against the original turn.
|
|
13
|
+
|
|
14
|
+
import crypto from 'node:crypto';
|
|
15
|
+
import fs from 'node:fs';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
|
|
18
|
+
import { openEventIndex } from './event-index.js';
|
|
19
|
+
import { acquireLease, releaseLease } from './lock.js';
|
|
20
|
+
import { resolveHostKey, readHostKey } from '../role/writeprotect.js';
|
|
21
|
+
|
|
22
|
+
export const AUTO_CARD_SCHEMA = 'auto-card/v1';
|
|
23
|
+
export const CARDS_REL = path.join('.longlive', 'index', 'cards.jsonl');
|
|
24
|
+
export const CARD_TITLE_MAX = 80;
|
|
25
|
+
export const CARD_ANCHOR_HINT_MAX = 60;
|
|
26
|
+
|
|
27
|
+
let cache = null;
|
|
28
|
+
|
|
29
|
+
export function cardsPathOf(memoryRoot) {
|
|
30
|
+
return path.join(path.resolve(String(memoryRoot)), CARDS_REL);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sha256Hex(text) {
|
|
34
|
+
return crypto.createHash('sha256').update(text, 'utf8').digest('hex');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function macHex(key, text) {
|
|
38
|
+
return crypto.createHmac('sha256', key).update(String(text), 'utf8').digest('hex');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Canonical signed payload: every trusted card field except the signature. */
|
|
42
|
+
export function autoCardPayload(card) {
|
|
43
|
+
return {
|
|
44
|
+
schema: card?.schema ?? AUTO_CARD_SCHEMA,
|
|
45
|
+
cardId: card?.cardId ?? '',
|
|
46
|
+
eventId: card?.eventId ?? '',
|
|
47
|
+
sourceEventId: card?.sourceEventId ?? card?.eventId ?? '',
|
|
48
|
+
sessionId: card?.sessionId ?? '',
|
|
49
|
+
turn: card?.turn ?? null,
|
|
50
|
+
at: card?.at ?? '',
|
|
51
|
+
kind: card?.kind ?? '',
|
|
52
|
+
title: card?.title ?? '',
|
|
53
|
+
terms: Array.isArray(card?.terms) ? card.terms : [],
|
|
54
|
+
anchor: card?.anchor ?? null,
|
|
55
|
+
salience: card?.salience ?? 1,
|
|
56
|
+
retention: card?.retention ?? null,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function signAutoCard(card, auditKey) {
|
|
61
|
+
if (!auditKey) return { ...card, sig: null };
|
|
62
|
+
return { ...card, sig: macHex(auditKey, JSON.stringify(autoCardPayload(card))) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function oneLine(text, max = 160) {
|
|
66
|
+
return String(text ?? '').replace(/\s+/gu, ' ').trim().slice(0, max);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function cardIdOf(eventId) {
|
|
70
|
+
return `card:${sha256Hex(String(eventId ?? '')).slice(0, 16)}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Full-file SHA-256: any byte change invalidates the cache, even mid-file. */
|
|
74
|
+
export function sampleContentHash(file, size = null) {
|
|
75
|
+
try {
|
|
76
|
+
return sha256Hex(fs.readFileSync(file));
|
|
77
|
+
} catch {
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function anchorOf(record) {
|
|
83
|
+
const anchor = Array.isArray(record?.anchors) && record.anchors.length > 0 ? record.anchors[0] : null;
|
|
84
|
+
if (!anchor || typeof anchor !== 'object') return null;
|
|
85
|
+
if (typeof anchor.sessionId !== 'string' || anchor.sessionId === '') return null;
|
|
86
|
+
if (!Number.isInteger(anchor.turn) || !Number.isInteger(anchor.seqStart) || !Number.isInteger(anchor.seqEnd)) return null;
|
|
87
|
+
if (typeof anchor.digest !== 'string' || !/^[0-9a-f]{64}$/.test(anchor.digest)) return null;
|
|
88
|
+
// Copy only stable anchor fields (no status mutation is written here).
|
|
89
|
+
return {
|
|
90
|
+
sessionId: anchor.sessionId,
|
|
91
|
+
turn: anchor.turn,
|
|
92
|
+
seqStart: anchor.seqStart,
|
|
93
|
+
seqEnd: anchor.seqEnd,
|
|
94
|
+
digest: anchor.digest,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Pure EventRecord → AutoCard (or null when the record is not anchorable). */
|
|
99
|
+
export function makeAutoCard(record, { now = () => new Date(), auditKey = null } = {}) {
|
|
100
|
+
if (!record || typeof record !== 'object') return null;
|
|
101
|
+
const eventId = typeof record.id === 'string' ? record.id : '';
|
|
102
|
+
const anchor = anchorOf(record);
|
|
103
|
+
if (eventId === '' || anchor === null) return null;
|
|
104
|
+
// Turn 0 is the session preamble (system/tool scaffolding), not a user turn.
|
|
105
|
+
if (!Number.isInteger(record.turn) || record.turn < 1) return null;
|
|
106
|
+
const atMs = Number.isFinite(record.timeEnd) ? record.timeEnd
|
|
107
|
+
: (Number.isFinite(record.timeStart) ? record.timeStart : now().getTime());
|
|
108
|
+
const title = oneLine(record.summary, CARD_TITLE_MAX);
|
|
109
|
+
const terms = Array.isArray(record.terms)
|
|
110
|
+
? [...new Set(record.terms.map((term) => oneLine(term, 40)).filter((term) => term !== ''))].slice(0, 20)
|
|
111
|
+
: [];
|
|
112
|
+
return {
|
|
113
|
+
schema: AUTO_CARD_SCHEMA,
|
|
114
|
+
cardId: cardIdOf(eventId),
|
|
115
|
+
eventId,
|
|
116
|
+
sourceEventId: eventId,
|
|
117
|
+
sessionId: String(record.sessionId ?? anchor.sessionId),
|
|
118
|
+
turn: Number.isInteger(record.turn) ? record.turn : anchor.turn,
|
|
119
|
+
at: new Date(atMs).toISOString(),
|
|
120
|
+
kind: oneLine(record.kind, 40) || 'chat',
|
|
121
|
+
title,
|
|
122
|
+
terms,
|
|
123
|
+
anchor,
|
|
124
|
+
// Event cards decay fast but are never physically deleted (append-only,
|
|
125
|
+
// no forget execution); salience is intentionally lower than curated cards.
|
|
126
|
+
salience: 1,
|
|
127
|
+
retention: { decay: 'fast', ttlDays: 30 },
|
|
128
|
+
...(auditKey ? { sig: macHex(auditKey, JSON.stringify(autoCardPayload({
|
|
129
|
+
schema: AUTO_CARD_SCHEMA,
|
|
130
|
+
cardId: cardIdOf(eventId),
|
|
131
|
+
eventId,
|
|
132
|
+
sourceEventId: eventId,
|
|
133
|
+
sessionId: String(record.sessionId ?? anchor.sessionId),
|
|
134
|
+
turn: Number.isInteger(record.turn) ? record.turn : anchor.turn,
|
|
135
|
+
at: new Date(atMs).toISOString(),
|
|
136
|
+
kind: oneLine(record.kind, 40) || 'chat',
|
|
137
|
+
title,
|
|
138
|
+
terms,
|
|
139
|
+
anchor,
|
|
140
|
+
salience: 1,
|
|
141
|
+
retention: { decay: 'fast', ttlDays: 30 },
|
|
142
|
+
}))) } : {}),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function readAutoCards(memoryRoot) {
|
|
147
|
+
const file = cardsPathOf(memoryRoot);
|
|
148
|
+
let stat = null;
|
|
149
|
+
try { stat = fs.statSync(file); } catch { cache = { file: null, mtimeMs: 0, size: 0, contentHash: '', cards: [] }; return cache.cards; }
|
|
150
|
+
const contentHash = sampleContentHash(file, stat.size);
|
|
151
|
+
if (cache && cache.file === file && cache.mtimeMs === stat.mtimeMs && cache.size === stat.size
|
|
152
|
+
&& cache.contentHash === contentHash) return cache.cards;
|
|
153
|
+
const out = [];
|
|
154
|
+
let raw = '';
|
|
155
|
+
try { raw = fs.readFileSync(file, 'utf8'); } catch { return out; }
|
|
156
|
+
for (const line of raw.split('\n')) {
|
|
157
|
+
if (line.trim() === '') continue;
|
|
158
|
+
try {
|
|
159
|
+
const card = JSON.parse(line);
|
|
160
|
+
if (!card || typeof card.cardId !== 'string' || typeof card.eventId !== 'string') continue;
|
|
161
|
+
// Reject injection-shaped raw rows early (the trusted read path also
|
|
162
|
+
// verifies the HMAC + EventRecord; this keeps inspection APIs inert).
|
|
163
|
+
const unsafe = (value) => typeof value === 'string' && /[\u0000-\u001f\u007f-\u009f\u202a-\u202e]/u.test(value);
|
|
164
|
+
const unsafeAnchor = card.anchor && typeof card.anchor.sessionId === 'string' && unsafe(card.anchor.sessionId);
|
|
165
|
+
const unsafeTerms = Array.isArray(card.terms) && card.terms.some((term) => unsafe(term));
|
|
166
|
+
if (unsafe(card.title) || unsafeTerms || unsafeAnchor) continue;
|
|
167
|
+
out.push(card);
|
|
168
|
+
} catch { /* corrupt line is skipped; append-only file is never rewritten */ }
|
|
169
|
+
}
|
|
170
|
+
cache = { file, mtimeMs: stat.mtimeMs, size: stat.size, contentHash, cards: out };
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** True when the card's anchor is exactly the EventRecord's primary anchor. */
|
|
175
|
+
export function validateAutoCardRecord(card, event, auditKey = null) {
|
|
176
|
+
if (!card || !event) return { ok: false, code: 'E_CARD_EVENT_MISSING' };
|
|
177
|
+
if (!auditKey || typeof card.sig !== 'string' || card.sig === '') return { ok: false, code: 'E_CARD_UNSIGNED' };
|
|
178
|
+
const expectedSig = macHex(auditKey, JSON.stringify(autoCardPayload(card)));
|
|
179
|
+
if (card.sig !== expectedSig) return { ok: false, code: 'E_CARD_SIG_INVALID' };
|
|
180
|
+
if (typeof card.eventId !== 'string' || card.eventId !== event.id) return { ok: false, code: 'E_CARD_EVENT_MISMATCH' };
|
|
181
|
+
if (card.sourceEventId !== event.id) return { ok: false, code: 'E_CARD_SOURCE_MISMATCH' };
|
|
182
|
+
if (card.cardId !== cardIdOf(card.eventId)) return { ok: false, code: 'E_CARD_ID_MISMATCH' };
|
|
183
|
+
const anchor = card.anchor;
|
|
184
|
+
const expected = Array.isArray(event.anchors) ? event.anchors[0] : null;
|
|
185
|
+
if (!anchor || !expected) return { ok: false, code: 'E_CARD_ANCHOR_MISSING' };
|
|
186
|
+
for (const key of ['sessionId', 'turn', 'seqStart', 'seqEnd', 'digest']) {
|
|
187
|
+
if (anchor[key] !== expected[key]) return { ok: false, code: 'E_CARD_ANCHOR_MISMATCH' };
|
|
188
|
+
}
|
|
189
|
+
if (anchor.digest !== event.digest) return { ok: false, code: 'E_CARD_DIGEST_MISMATCH' };
|
|
190
|
+
return { ok: true, code: 'OK' };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Trusted content is re-derived from the EventRecord, never from the card file. */
|
|
194
|
+
export function trustedCardFromEvent(card, event) {
|
|
195
|
+
const anchor = Array.isArray(event?.anchors) ? event.anchors[0] : null;
|
|
196
|
+
const atMs = Number.isFinite(event?.timeEnd) ? event.timeEnd
|
|
197
|
+
: (Number.isFinite(event?.timeStart) ? event.timeStart : Date.parse(card?.at ?? '') || 0);
|
|
198
|
+
return {
|
|
199
|
+
schema: AUTO_CARD_SCHEMA,
|
|
200
|
+
cardId: cardIdOf(event.id),
|
|
201
|
+
eventId: event.id,
|
|
202
|
+
sourceEventId: event.id,
|
|
203
|
+
sessionId: String(event.sessionId ?? anchor?.sessionId ?? ''),
|
|
204
|
+
turn: Number.isInteger(event.turn) ? event.turn : anchor?.turn,
|
|
205
|
+
at: new Date(atMs).toISOString(),
|
|
206
|
+
kind: oneLine(event.kind, 40) || 'chat',
|
|
207
|
+
title: oneLine(event.summary, CARD_TITLE_MAX),
|
|
208
|
+
terms: Array.isArray(event.terms) ? event.terms.slice(0, 20) : [],
|
|
209
|
+
anchor: anchor ? {
|
|
210
|
+
sessionId: anchor.sessionId,
|
|
211
|
+
turn: anchor.turn,
|
|
212
|
+
seqStart: anchor.seqStart,
|
|
213
|
+
seqEnd: anchor.seqEnd,
|
|
214
|
+
digest: anchor.digest,
|
|
215
|
+
} : null,
|
|
216
|
+
salience: 1,
|
|
217
|
+
retention: { decay: 'fast', ttlDays: 30 },
|
|
218
|
+
sig: card?.sig ?? null,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function auditInvalidCard(memoryRoot, card, result) {
|
|
223
|
+
try {
|
|
224
|
+
const file = path.join(path.resolve(String(memoryRoot)), '.longlive', 'index', 'card-audit.jsonl');
|
|
225
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
226
|
+
fs.appendFileSync(file, `${JSON.stringify({
|
|
227
|
+
schema: 'card-audit/v1',
|
|
228
|
+
at: new Date().toISOString(),
|
|
229
|
+
action: 'drop',
|
|
230
|
+
cardId: card?.cardId ?? null,
|
|
231
|
+
eventId: card?.eventId ?? null,
|
|
232
|
+
code: result?.code ?? 'E_CARD_INVALID',
|
|
233
|
+
})}\n`, 'utf8');
|
|
234
|
+
} catch { /* audit failure must not turn into a recall hit */ }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let verifiedCache = null;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Production reader: only cards whose source EventRecord exists in events.db
|
|
241
|
+
* and whose anchor/digest match that record are returned. Invalid cards are
|
|
242
|
+
* dropped and audited; they never reach recall/injection.
|
|
243
|
+
*/
|
|
244
|
+
export function readVerifiedAutoCards({ memoryRoot } = {}) {
|
|
245
|
+
const root = path.resolve(String(memoryRoot ?? ''));
|
|
246
|
+
const file = cardsPathOf(root);
|
|
247
|
+
let stat;
|
|
248
|
+
try { stat = fs.statSync(file); } catch { return []; }
|
|
249
|
+
const dbPath = path.join(root, '.longlive', 'index', 'events.db');
|
|
250
|
+
let dbStat;
|
|
251
|
+
try { dbStat = fs.statSync(dbPath); } catch { return []; } // no source of truth => fail closed
|
|
252
|
+
const contentHash = sampleContentHash(file, stat.size);
|
|
253
|
+
const auditKey = readHostKey(root);
|
|
254
|
+
const key = `${file}|${stat.mtimeMs}|${stat.size}|${contentHash}|${dbStat.mtimeMs}|${dbStat.size}|${auditKey ? sha256Hex(auditKey).slice(0, 16) : 'nokey'}`;
|
|
255
|
+
if (verifiedCache && verifiedCache.key === key) return verifiedCache.cards;
|
|
256
|
+
const cards = readAutoCards(root);
|
|
257
|
+
if (!auditKey) {
|
|
258
|
+
for (const card of cards) auditInvalidCard(root, card, { code: 'E_NO_KEY' });
|
|
259
|
+
verifiedCache = { key, cards: [] };
|
|
260
|
+
return [];
|
|
261
|
+
}
|
|
262
|
+
const index = openEventIndex({ memoryRoot: root });
|
|
263
|
+
const verified = [];
|
|
264
|
+
const seen = new Map();
|
|
265
|
+
try {
|
|
266
|
+
for (const card of cards) {
|
|
267
|
+
if (seen.has(card.eventId)) {
|
|
268
|
+
// The first valid signed card wins; a later forged duplicate can never
|
|
269
|
+
// suppress the genuine card (write order alone is not authority).
|
|
270
|
+
auditInvalidCard(root, card, { code: 'E_CARD_DUPLICATE' });
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
const event = index.getEvent(card.eventId);
|
|
274
|
+
const result = validateAutoCardRecord(card, event, auditKey);
|
|
275
|
+
if (result.ok) {
|
|
276
|
+
const trusted = trustedCardFromEvent(card, event);
|
|
277
|
+
verified.push(trusted);
|
|
278
|
+
seen.set(card.eventId, trusted);
|
|
279
|
+
} else auditInvalidCard(root, card, result);
|
|
280
|
+
}
|
|
281
|
+
} finally {
|
|
282
|
+
index.close();
|
|
283
|
+
}
|
|
284
|
+
verifiedCache = { key, cards: verified };
|
|
285
|
+
return verified;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Append cards for records, idempotent by eventId.
|
|
290
|
+
* `dryRun` performs the same read/dedupe pass without writing.
|
|
291
|
+
*/
|
|
292
|
+
export function appendAutoCards({ memoryRoot, records, now = () => new Date(), dryRun = false, skipLock = false, resign = false } = {}) {
|
|
293
|
+
const root = path.resolve(String(memoryRoot ?? ''));
|
|
294
|
+
if (root === '') throw new Error('E_MEMORY_ROOT: appendAutoCards requires memoryRoot');
|
|
295
|
+
const run = () => {
|
|
296
|
+
const file = cardsPathOf(root);
|
|
297
|
+
const existingCards = readAutoCards(root);
|
|
298
|
+
const existing = new Map(existingCards.map((card) => [card.eventId, card]));
|
|
299
|
+
const existingByCardId = new Map(existingCards.map((card) => [card.cardId, card]));
|
|
300
|
+
let auditKey = null;
|
|
301
|
+
try { auditKey = resolveHostKey(root); } catch { auditKey = null; }
|
|
302
|
+
const cards = [];
|
|
303
|
+
let skipped = 0;
|
|
304
|
+
for (const record of Array.isArray(records) ? records : []) {
|
|
305
|
+
const card = makeAutoCard(record, { now, auditKey });
|
|
306
|
+
if (card === null) { skipped += 1; continue; }
|
|
307
|
+
const sameCardId = existingByCardId.get(card.cardId);
|
|
308
|
+
if (sameCardId && sameCardId.eventId !== card.eventId) {
|
|
309
|
+
auditInvalidCard(root, card, { code: 'E_CARD_ID_COLLISION' });
|
|
310
|
+
skipped += 1;
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
const prior = existing.get(card.eventId);
|
|
314
|
+
if (prior && !resign) {
|
|
315
|
+
// A signed card may replace an old unsigned draft (migration redo);
|
|
316
|
+
// otherwise eventId is already final.
|
|
317
|
+
if (prior.sig && card.sig) { skipped += 1; continue; }
|
|
318
|
+
if (prior.sig && !card.sig) { skipped += 1; continue; }
|
|
319
|
+
if (!card.sig) { skipped += 1; continue; }
|
|
320
|
+
}
|
|
321
|
+
if (resign && !card.sig) { skipped += 1; continue; }
|
|
322
|
+
existing.set(card.eventId, card);
|
|
323
|
+
existingByCardId.set(card.cardId, card);
|
|
324
|
+
cards.push(card);
|
|
325
|
+
}
|
|
326
|
+
if (!dryRun && cards.length > 0) {
|
|
327
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
328
|
+
fs.appendFileSync(file, cards.map((card) => JSON.stringify(card)).join('\n') + '\n', 'utf8');
|
|
329
|
+
}
|
|
330
|
+
return { ok: true, file, written: cards.length, skipped, cards, dryRun };
|
|
331
|
+
};
|
|
332
|
+
if (skipLock || dryRun) return run();
|
|
333
|
+
const lockFile = path.join(root, '.longlive', 'memory', 'cards.lock');
|
|
334
|
+
fs.mkdirSync(path.dirname(lockFile), { recursive: true });
|
|
335
|
+
const holder = `cards-${process.pid}-${Date.now()}`;
|
|
336
|
+
const deadline = Date.now() + 3000;
|
|
337
|
+
let lease = null;
|
|
338
|
+
for (;;) {
|
|
339
|
+
try {
|
|
340
|
+
lease = acquireLease(lockFile, { holder });
|
|
341
|
+
break;
|
|
342
|
+
} catch (error) {
|
|
343
|
+
if (error?.code !== 'E_LOCKED' || Date.now() >= deadline) throw error;
|
|
344
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
try {
|
|
348
|
+
return run();
|
|
349
|
+
} finally {
|
|
350
|
+
releaseLease(lockFile, holder);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** One-line anchor hint for injection, capped at 60 chars. */
|
|
355
|
+
export function anchorHintOf(card) {
|
|
356
|
+
const anchor = card?.anchor;
|
|
357
|
+
if (!anchor || typeof anchor.sessionId !== 'string' || !Number.isInteger(anchor.turn)) return '';
|
|
358
|
+
const suffix = `#${anchor.turn}]`;
|
|
359
|
+
const prefix = '[原文 ';
|
|
360
|
+
const maxSession = Math.max(1, CARD_ANCHOR_HINT_MAX - prefix.length - suffix.length);
|
|
361
|
+
const session = anchor.sessionId.length > maxSession ? anchor.sessionId.slice(0, maxSession) : anchor.sessionId;
|
|
362
|
+
return `${prefix}${session}${suffix}`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function bigrams(text) {
|
|
366
|
+
const value = String(text ?? '').toLowerCase().replace(/\s+/gu, ' ').trim();
|
|
367
|
+
const out = new Set();
|
|
368
|
+
for (let i = 0; i < value.length - 1; i += 1) out.add(value.slice(i, i + 2));
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function cardScore(card, query) {
|
|
373
|
+
const q = String(query ?? '').trim().toLowerCase();
|
|
374
|
+
if (q === '') return 0;
|
|
375
|
+
const title = String(card.title ?? '').toLowerCase();
|
|
376
|
+
const terms = Array.isArray(card.terms) ? card.terms.join(' ').toLowerCase() : '';
|
|
377
|
+
let score = 0;
|
|
378
|
+
if (title.includes(q)) score += 5;
|
|
379
|
+
if (terms.includes(q)) score += 4;
|
|
380
|
+
const qb = bigrams(q);
|
|
381
|
+
const tb = bigrams(`${title} ${terms}`);
|
|
382
|
+
if (qb.size > 0) {
|
|
383
|
+
let hit = 0;
|
|
384
|
+
for (const gram of qb) if (tb.has(gram)) hit += 1;
|
|
385
|
+
score += (hit / qb.size) * 2;
|
|
386
|
+
}
|
|
387
|
+
return score;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/** Deterministic keyword/term search over trusted AutoCards. */
|
|
391
|
+
export function searchAutoCards({ memoryRoot, query, limit = 5, verified = true } = {}) {
|
|
392
|
+
const q = String(query ?? '').trim();
|
|
393
|
+
const max = Math.max(1, Math.min(20, Number(limit) || 5));
|
|
394
|
+
if (q === '') return [];
|
|
395
|
+
return (verified ? readVerifiedAutoCards({ memoryRoot }) : readAutoCards(memoryRoot))
|
|
396
|
+
.map((card) => ({ card, score: cardScore(card, q) }))
|
|
397
|
+
.filter((row) => row.score > 0)
|
|
398
|
+
.sort((a, b) => b.score - a.score || a.card.cardId.localeCompare(b.card.cardId))
|
|
399
|
+
.slice(0, max)
|
|
400
|
+
.map((row) => row.card);
|
|
401
|
+
}
|