@jjh32456/dsh-longlive 1.6.8 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -1539
- package/LICENSE +1 -1
- package/QUICKSTART.md +33 -12
- package/README.en.md +17 -7
- package/README.md +29 -10
- package/SPEC.md +5 -5
- package/cordis.patch.yml +17 -12
- package/docs/ANSWER-GUARD.md +92 -0
- package/docs/ARCHITECTURE.md +3 -3
- package/docs/AUTO-RECALL-WIRING.md +1 -1
- package/docs/AUTO-RECALL.md +15 -9
- package/docs/CONFIG.md +143 -37
- package/docs/ERRORS.md +17 -5
- package/docs/FAQ.md +7 -7
- package/docs/FEATURES.md +66 -29
- 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 +3 -2
- package/docs/RESIDENT-WATCHER.md +1 -1
- package/docs/SESSION-WATCH.md +6 -6
- package/docs/TROUBLESHOOTING.md +4 -4
- package/package.json +42 -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 +129 -18
- package/src/activity/model.js +11 -1
- package/src/activity/store.js +22 -3
- 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 +145 -48
- package/src/deps.js +6 -4
- package/src/features.js +158 -8
- 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/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 +1 -1
- 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/config.mjs +5 -5
- package/src/router/service/context.mjs +1 -1
- package/src/router/service/queue.mjs +1 -1
- 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/activity-realtime.test.mjs +0 -959
- package/test/activity-stream.test.mjs +0 -512
- package/test/auto-recall.test.mjs +0 -474
- package/test/auto-truncate.test.mjs +0 -931
- package/test/autobind.test.mjs +0 -399
- 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 -210
- 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 -585
- package/test/filter-shadow.test.mjs +0 -170
- package/test/fixtures/activity-append-child.mjs +0 -29
- 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 -1023
- 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 -184
- 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 -1076
- 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 -315
- package/test/run.mjs +0 -2469
- package/test/safety-defaults.test.mjs +0 -181
- package/test/seeded-load.test.mjs +0 -436
- package/test/session-original.test.mjs +0 -338
- package/test/state-truth.test.mjs +0 -937
- 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
- package/test/version-consistency.test.mjs +0 -286
|
@@ -1,538 +0,0 @@
|
|
|
1
|
-
// B1/B2: daily maintenance scheduling (2026-11 feature batch).
|
|
2
|
-
//
|
|
3
|
-
// Every test runs against a throwaway memory root. The scheduler tests inject a
|
|
4
|
-
// fake clock and fake timers, so a "day" costs microseconds and no real timer
|
|
5
|
-
// is left behind.
|
|
6
|
-
|
|
7
|
-
import test from 'node:test';
|
|
8
|
-
import assert from 'node:assert/strict';
|
|
9
|
-
import fs from 'node:fs';
|
|
10
|
-
import os from 'node:os';
|
|
11
|
-
import path from 'node:path';
|
|
12
|
-
|
|
13
|
-
import {
|
|
14
|
-
createMaintenanceScheduler,
|
|
15
|
-
nextLocalMidnight,
|
|
16
|
-
pendingReportPath,
|
|
17
|
-
readScheduleState,
|
|
18
|
-
renderForgetPending,
|
|
19
|
-
runDailyMaintenance,
|
|
20
|
-
} from '../src/memory/maintenance.js';
|
|
21
|
-
import { normalizeFeatureConfig, registerFeatures } from '../src/features.js';
|
|
22
|
-
|
|
23
|
-
const DAY1 = new Date(2026, 10, 5, 10, 0, 0); // 2026-11-05 10:00 local
|
|
24
|
-
const DAY2 = new Date(2026, 10, 6, 9, 0, 0);
|
|
25
|
-
|
|
26
|
-
function cardFile({
|
|
27
|
-
title, salience = 3, strength = 3, lastUsed = null, status = null, tags = [],
|
|
28
|
-
}) {
|
|
29
|
-
return [
|
|
30
|
-
'---',
|
|
31
|
-
`title: ${title}`,
|
|
32
|
-
...(tags.length ? [`tags: [${tags.join(', ')}]`] : []),
|
|
33
|
-
...(status ? [`status: ${status}`] : []),
|
|
34
|
-
`salience: ${salience}`,
|
|
35
|
-
`strength: ${strength}`,
|
|
36
|
-
...(lastUsed ? [`last_used: ${lastUsed}`] : []),
|
|
37
|
-
'revisions: 0',
|
|
38
|
-
'---',
|
|
39
|
-
`# ${title}`,
|
|
40
|
-
'',
|
|
41
|
-
`${title}的正文。`,
|
|
42
|
-
'',
|
|
43
|
-
].join('\n');
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function journalFile(day, lines) {
|
|
47
|
-
return [
|
|
48
|
-
'---',
|
|
49
|
-
`title: ${day}`,
|
|
50
|
-
`date: ${day}`,
|
|
51
|
-
'salience: 3',
|
|
52
|
-
'strength: 3',
|
|
53
|
-
'revisions: 0',
|
|
54
|
-
'---',
|
|
55
|
-
'',
|
|
56
|
-
...lines,
|
|
57
|
-
'',
|
|
58
|
-
].join('\n');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function put(root, rel, content) {
|
|
62
|
-
const file = path.join(root, rel);
|
|
63
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
64
|
-
fs.writeFileSync(file, content, 'utf8');
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** A root with one fresh card, one epochs proposal, one tombstone proposal,
|
|
68
|
-
* two immune cards (salience=locked), and one card without last_used. */
|
|
69
|
-
function prepareRoot() {
|
|
70
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'longlive-maint-'));
|
|
71
|
-
fs.mkdirSync(path.join(root, 'topics'), { recursive: true });
|
|
72
|
-
fs.mkdirSync(path.join(root, 'journal'), { recursive: true });
|
|
73
|
-
put(root, 'journal/2026-11-04.md', journalFile('2026-11-04', ['今天定了:先做 B1,再做 B2。']));
|
|
74
|
-
put(root, 'topics/fresh.md', cardFile({ title: '新鲜', strength: 1, lastUsed: '2026-11-04' }));
|
|
75
|
-
put(root, 'topics/epochs.md', cardFile({ title: '三个月', strength: 1, lastUsed: '2026-07-01' }));
|
|
76
|
-
put(root, 'topics/tombstone.md', cardFile({ title: '一年以上', strength: 9, lastUsed: '2025-01-01' }));
|
|
77
|
-
put(root, 'topics/core.md', cardFile({ title: '核心', salience: 5, strength: 0, lastUsed: '2020-01-01' }));
|
|
78
|
-
put(root, 'topics/locked.md', cardFile({
|
|
79
|
-
title: '锁定', status: 'locked', salience: 5, strength: 1, lastUsed: '2020-01-01',
|
|
80
|
-
}));
|
|
81
|
-
put(root, 'topics/undated.md', cardFile({ title: '无日期', strength: 0 }));
|
|
82
|
-
return root;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/** Read every card-ish file (everything outside .longlive) for a 0-change check. */
|
|
86
|
-
function snapshotCards(root) {
|
|
87
|
-
const out = {};
|
|
88
|
-
const walk = (dir, rel) => {
|
|
89
|
-
for (const name of fs.readdirSync(dir).sort()) {
|
|
90
|
-
if (rel === '' && name === '.longlive') continue;
|
|
91
|
-
const abs = path.join(dir, name);
|
|
92
|
-
const nextRel = rel ? `${rel}/${name}` : name;
|
|
93
|
-
const stat = fs.statSync(abs);
|
|
94
|
-
if (stat.isDirectory()) walk(abs, nextRel);
|
|
95
|
-
else out[nextRel] = fs.readFileSync(abs, 'utf8');
|
|
96
|
-
}
|
|
97
|
-
};
|
|
98
|
-
walk(root, '');
|
|
99
|
-
return out;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function readSchedule(root) {
|
|
103
|
-
return JSON.parse(fs.readFileSync(path.join(root, '.longlive', 'bridge', 'schedule.json'), 'utf8'));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function fakeTimers() {
|
|
107
|
-
const jobs = [];
|
|
108
|
-
let nextId = 0;
|
|
109
|
-
return {
|
|
110
|
-
setTimeout(fn, ms) {
|
|
111
|
-
const handle = { id: (nextId += 1), ms, fn };
|
|
112
|
-
jobs.push(handle);
|
|
113
|
-
return handle;
|
|
114
|
-
},
|
|
115
|
-
clearTimeout(handle) {
|
|
116
|
-
const at = jobs.indexOf(handle);
|
|
117
|
-
if (at >= 0) jobs.splice(at, 1);
|
|
118
|
-
},
|
|
119
|
-
count() {
|
|
120
|
-
return jobs.length;
|
|
121
|
-
},
|
|
122
|
-
delays() {
|
|
123
|
-
return jobs.map((job) => job.ms);
|
|
124
|
-
},
|
|
125
|
-
next() {
|
|
126
|
-
return jobs[0] ?? null;
|
|
127
|
-
},
|
|
128
|
-
async fire(handle = null) {
|
|
129
|
-
const job = handle ?? jobs.shift();
|
|
130
|
-
if (!job) throw new Error('no pending timer');
|
|
131
|
-
const at = jobs.indexOf(job);
|
|
132
|
-
if (at >= 0) jobs.splice(at, 1);
|
|
133
|
-
return job.fn();
|
|
134
|
-
},
|
|
135
|
-
};
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
test('day one: both reports + schedule state are written, cards are unchanged', () => {
|
|
139
|
-
const root = prepareRoot();
|
|
140
|
-
const before = snapshotCards(root);
|
|
141
|
-
const result = runDailyMaintenance({ memoryRoot: root, now: DAY1, config: {} });
|
|
142
|
-
|
|
143
|
-
assert.equal(result.ok, true);
|
|
144
|
-
assert.equal(result.ran, true);
|
|
145
|
-
assert.equal(result.day, '2026-11-05');
|
|
146
|
-
assert.equal(result.consolidate.status, 'ran');
|
|
147
|
-
assert.equal(result.consolidate.reportRel, '.longlive/reports/consolidate-2026-11-05.md');
|
|
148
|
-
assert.ok(result.consolidate.proposals >= 1, 'consolidate should see the journal paragraph');
|
|
149
|
-
assert.ok(fs.existsSync(path.join(root, result.consolidate.reportRel)));
|
|
150
|
-
|
|
151
|
-
assert.equal(result.forget.status, 'ran');
|
|
152
|
-
assert.equal(result.forget.pending, 2);
|
|
153
|
-
assert.equal(result.forget.pendingRel, '.longlive/reports/forget-pending-20261105.md');
|
|
154
|
-
assert.equal(result.forget.immune, 2);
|
|
155
|
-
assert.equal(result.forget.undated, 2);
|
|
156
|
-
|
|
157
|
-
const state = readSchedule(root);
|
|
158
|
-
assert.equal(state.lastConsolidateDate, '2026-11-05');
|
|
159
|
-
assert.equal(state.lastForgetDate, '2026-11-05');
|
|
160
|
-
assert.equal(state.lastForgetPendingCount, 2);
|
|
161
|
-
assert.equal(state.runs.length, 1);
|
|
162
|
-
assert.equal(state.runs[0].consolidate, 'ran');
|
|
163
|
-
assert.equal(state.runs[0].forget, 'ran');
|
|
164
|
-
|
|
165
|
-
const pending = fs.readFileSync(path.join(root, result.forget.pendingRel), 'utf8');
|
|
166
|
-
assert.match(pending, /confirm:"FORGET"/);
|
|
167
|
-
assert.match(pending, /topics\/epochs\.md/);
|
|
168
|
-
assert.match(pending, /topics\/tombstone\.md/);
|
|
169
|
-
assert.match(pending, /降级/);
|
|
170
|
-
assert.match(pending, /立碑/);
|
|
171
|
-
assert.match(pending, /免疫/);
|
|
172
|
-
assert.match(pending, /没有移动、删除或改写任何卡片/);
|
|
173
|
-
|
|
174
|
-
assert.deepEqual(snapshotCards(root), before, 'B1/B2 must not touch a single card');
|
|
175
|
-
assert.equal(
|
|
176
|
-
fs.existsSync(path.join(root, '.longlive', 'bridge', 'schedule.lock')),
|
|
177
|
-
false,
|
|
178
|
-
'the cross-process lock must be released',
|
|
179
|
-
);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
test('a live lock from another session skips the run; a stale lock is reclaimed', () => {
|
|
183
|
-
const root = prepareRoot();
|
|
184
|
-
const lock = path.join(root, '.longlive', 'bridge', 'schedule.lock');
|
|
185
|
-
fs.mkdirSync(path.dirname(lock), { recursive: true });
|
|
186
|
-
fs.writeFileSync(lock, JSON.stringify({ pid: 999999, day: '2026-11-05' }));
|
|
187
|
-
|
|
188
|
-
const skipped = runDailyMaintenance({ memoryRoot: root, now: DAY1, config: {} });
|
|
189
|
-
assert.equal(skipped.ran, false);
|
|
190
|
-
assert.equal(skipped.reason, 'locked');
|
|
191
|
-
assert.equal(fs.existsSync(path.join(root, '.longlive', 'reports', 'forget-pending-20261105.md')), false);
|
|
192
|
-
assert.equal(fs.existsSync(lock), true, 'a live lock is not stolen');
|
|
193
|
-
|
|
194
|
-
const old = new Date(Date.now() - 11 * 60 * 1000);
|
|
195
|
-
fs.utimesSync(lock, old, old);
|
|
196
|
-
const reclaimed = runDailyMaintenance({ memoryRoot: root, now: DAY1, config: {} });
|
|
197
|
-
assert.equal(reclaimed.ran, true);
|
|
198
|
-
assert.equal(fs.existsSync(lock), false);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test('same local day is idempotent: no second report, state bytes unchanged', () => {
|
|
202
|
-
const root = prepareRoot();
|
|
203
|
-
runDailyMaintenance({ memoryRoot: root, now: DAY1, config: {} });
|
|
204
|
-
const stateFile = path.join(root, '.longlive', 'bridge', 'schedule.json');
|
|
205
|
-
const pendingFile = path.join(root, '.longlive', 'reports', 'forget-pending-20261105.md');
|
|
206
|
-
const stateBefore = fs.readFileSync(stateFile, 'utf8');
|
|
207
|
-
const pendingBefore = fs.readFileSync(pendingFile, 'utf8');
|
|
208
|
-
|
|
209
|
-
const result = runDailyMaintenance({ memoryRoot: root, now: new Date(2026, 10, 5, 23, 59, 0), config: {} });
|
|
210
|
-
assert.equal(result.ok, true);
|
|
211
|
-
assert.equal(result.ran, false);
|
|
212
|
-
assert.equal(result.reason, 'already-ran-today');
|
|
213
|
-
assert.equal(fs.readFileSync(stateFile, 'utf8'), stateBefore);
|
|
214
|
-
assert.equal(fs.readFileSync(pendingFile, 'utf8'), pendingBefore);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
test('a new local date triggers a second run', () => {
|
|
218
|
-
const root = prepareRoot();
|
|
219
|
-
runDailyMaintenance({ memoryRoot: root, now: DAY1, config: {} });
|
|
220
|
-
const result = runDailyMaintenance({ memoryRoot: root, now: DAY2, config: {} });
|
|
221
|
-
|
|
222
|
-
assert.equal(result.ran, true);
|
|
223
|
-
assert.equal(result.day, '2026-11-06');
|
|
224
|
-
assert.equal(result.consolidate.reportRel, '.longlive/reports/consolidate-2026-11-06.md');
|
|
225
|
-
assert.ok(fs.existsSync(path.join(root, result.consolidate.reportRel)));
|
|
226
|
-
|
|
227
|
-
const state = readSchedule(root);
|
|
228
|
-
assert.equal(state.lastConsolidateDate, '2026-11-06');
|
|
229
|
-
assert.equal(state.lastForgetDate, '2026-11-06');
|
|
230
|
-
assert.equal(state.runs.length, 2);
|
|
231
|
-
assert.equal(state.runs[1].day, '2026-11-06');
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
test('consolidate failure is recorded, does not stop forget and does not throw', () => {
|
|
235
|
-
const root = prepareRoot();
|
|
236
|
-
const before = snapshotCards(root);
|
|
237
|
-
const boom = () => {
|
|
238
|
-
const error = new Error('模拟巩固失败');
|
|
239
|
-
error.code = 'E_TEST_CONSOLIDATE';
|
|
240
|
-
throw error;
|
|
241
|
-
};
|
|
242
|
-
const result = runDailyMaintenance({
|
|
243
|
-
memoryRoot: root,
|
|
244
|
-
now: DAY1,
|
|
245
|
-
config: {},
|
|
246
|
-
consolidate: boom,
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
assert.equal(result.ok, true);
|
|
250
|
-
assert.equal(result.consolidate.status, 'failed');
|
|
251
|
-
assert.equal(result.consolidate.code, 'E_TEST_CONSOLIDATE');
|
|
252
|
-
assert.equal(result.forget.status, 'ran');
|
|
253
|
-
assert.ok(fs.existsSync(path.join(root, result.forget.pendingRel)));
|
|
254
|
-
|
|
255
|
-
const state = readSchedule(root);
|
|
256
|
-
assert.equal(state.lastConsolidateDate, null, 'a failed day must be retried');
|
|
257
|
-
assert.equal(state.lastForgetDate, '2026-11-05');
|
|
258
|
-
assert.equal(state.lastConsolidateError.code, 'E_TEST_CONSOLIDATE');
|
|
259
|
-
assert.equal(state.runs[0].consolidateError, 'E_TEST_CONSOLIDATE');
|
|
260
|
-
assert.deepEqual(snapshotCards(root), before);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test('forget failure leaves cards alone and keeps consolidate', () => {
|
|
264
|
-
const root = prepareRoot();
|
|
265
|
-
const before = snapshotCards(root);
|
|
266
|
-
const boom = () => {
|
|
267
|
-
const error = new Error('模拟遗忘失败');
|
|
268
|
-
error.code = 'E_TEST_FORGET';
|
|
269
|
-
throw error;
|
|
270
|
-
};
|
|
271
|
-
const result = runDailyMaintenance({
|
|
272
|
-
memoryRoot: root,
|
|
273
|
-
now: DAY1,
|
|
274
|
-
config: {},
|
|
275
|
-
forget: boom,
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
assert.equal(result.ok, true);
|
|
279
|
-
assert.equal(result.consolidate.status, 'ran');
|
|
280
|
-
assert.equal(result.forget.status, 'failed');
|
|
281
|
-
assert.equal(result.forget.code, 'E_TEST_FORGET');
|
|
282
|
-
assert.equal(fs.existsSync(pendingReportPath(root, '2026-11-05')), false);
|
|
283
|
-
|
|
284
|
-
const state = readSchedule(root);
|
|
285
|
-
assert.equal(state.lastForgetDate, null);
|
|
286
|
-
assert.equal(state.lastConsolidateDate, '2026-11-05');
|
|
287
|
-
assert.equal(state.lastForgetError.code, 'E_TEST_FORGET');
|
|
288
|
-
assert.deepEqual(snapshotCards(root), before);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
test('a forget runner that claims to apply is rejected as a failure (0 card changes)', () => {
|
|
292
|
-
const root = prepareRoot();
|
|
293
|
-
const before = snapshotCards(root);
|
|
294
|
-
const result = runDailyMaintenance({
|
|
295
|
-
memoryRoot: root,
|
|
296
|
-
now: DAY1,
|
|
297
|
-
config: {},
|
|
298
|
-
forget: () => ({ dryRun: false, applied: true, proposals: [] }),
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
assert.equal(result.forget.status, 'failed');
|
|
302
|
-
assert.equal(result.forget.code, 'E_MAINTENANCE_DRYRUN_REQUIRED');
|
|
303
|
-
assert.deepEqual(snapshotCards(root), before);
|
|
304
|
-
assert.equal(readSchedule(root).lastForgetDate, null);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
test('pending list rendering carries the confirm gate and the three epitaph questions', () => {
|
|
308
|
-
const text = renderForgetPending({
|
|
309
|
-
day: '2026-11-05',
|
|
310
|
-
at: DAY1,
|
|
311
|
-
result: {
|
|
312
|
-
scanned: 4,
|
|
313
|
-
proposals: [{
|
|
314
|
-
path: 'topics/old.md',
|
|
315
|
-
kind: 'epochs',
|
|
316
|
-
ageDays: 120,
|
|
317
|
-
reason: '90 天未用且 strength 低',
|
|
318
|
-
salience: 2,
|
|
319
|
-
strength: 1,
|
|
320
|
-
last_used: '2026-07-08',
|
|
321
|
-
destRel: 'epochs/topics/old.md',
|
|
322
|
-
epitaph: { reason: '90 天未用', affects: 'tags: 项目甲', stillUseful: '从 epochs/topics/old.md 移回' },
|
|
323
|
-
}],
|
|
324
|
-
immune: [{ path: 'topics/core.md', status: 'active', reason: 'salience=5 核心记忆' }],
|
|
325
|
-
undated: [{ path: 'topics/nodate.md', reason: '没有可用的 last_used' }],
|
|
326
|
-
scaffold: { missing: [], ready: true },
|
|
327
|
-
},
|
|
328
|
-
});
|
|
329
|
-
assert.match(text, /confirm:"FORGET"/);
|
|
330
|
-
assert.match(text, /为什么关:90 天未用/);
|
|
331
|
-
assert.match(text, /影响谁:tags: 项目甲/);
|
|
332
|
-
assert.match(text, /还有用吗:从 epochs\/topics\/old\.md 移回/);
|
|
333
|
-
assert.match(text, /免疫/);
|
|
334
|
-
assert.match(text, /没有可用的 last_used/);
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
test('scheduler: exactly one timer to the next local midnight, then re-arms', async () => {
|
|
338
|
-
const root = prepareRoot();
|
|
339
|
-
const clock = { value: new Date(2026, 10, 5, 10, 0, 0) };
|
|
340
|
-
const timers = fakeTimers();
|
|
341
|
-
const scheduler = createMaintenanceScheduler({
|
|
342
|
-
memoryRoot: root,
|
|
343
|
-
config: { runOnStart: false, consolidate: true, forget: true },
|
|
344
|
-
now: () => clock.value,
|
|
345
|
-
timers,
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
const started = scheduler.start();
|
|
349
|
-
assert.equal(started.mode, 'midnight');
|
|
350
|
-
assert.equal(timers.count(), 1, 'one-shot timer, not a polling loop');
|
|
351
|
-
const expectedDelay = nextLocalMidnight(clock.value).getTime() - clock.value.getTime();
|
|
352
|
-
assert.equal(started.delayMs, expectedDelay);
|
|
353
|
-
assert.ok(started.delayMs > 0 && started.delayMs <= 24 * 60 * 60 * 1000 + 1000);
|
|
354
|
-
|
|
355
|
-
clock.value = new Date(2026, 10, 6, 0, 0, 2);
|
|
356
|
-
const result = await timers.fire(timers.next());
|
|
357
|
-
assert.equal(result.ran, true);
|
|
358
|
-
assert.equal(result.day, '2026-11-06');
|
|
359
|
-
assert.equal(result.consolidate.status, 'ran');
|
|
360
|
-
assert.equal(timers.count(), 1, 'the next midnight is armed after the run');
|
|
361
|
-
assert.ok(timers.delays()[0] > 23 * 60 * 60 * 1000);
|
|
362
|
-
});
|
|
363
|
-
|
|
364
|
-
test('scheduler: startup tick is short and then arms midnight; stop clears the timer', async () => {
|
|
365
|
-
const root = prepareRoot();
|
|
366
|
-
const clock = { value: new Date(2026, 10, 5, 10, 0, 0) };
|
|
367
|
-
const timers = fakeTimers();
|
|
368
|
-
const scheduler = createMaintenanceScheduler({
|
|
369
|
-
memoryRoot: root,
|
|
370
|
-
config: { runOnStart: true, startDelayMs: 0, consolidate: true, forget: true },
|
|
371
|
-
now: () => clock.value,
|
|
372
|
-
timers,
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
scheduler.start();
|
|
376
|
-
assert.deepEqual(timers.delays(), [0]);
|
|
377
|
-
const result = await timers.fire(timers.next());
|
|
378
|
-
assert.equal(result.ran, true);
|
|
379
|
-
assert.equal(result.day, '2026-11-05');
|
|
380
|
-
assert.equal(timers.count(), 1);
|
|
381
|
-
assert.ok(timers.delays()[0] > 0);
|
|
382
|
-
|
|
383
|
-
scheduler.stop();
|
|
384
|
-
assert.equal(timers.count(), 0);
|
|
385
|
-
const afterStop = await scheduler.runNow('manual');
|
|
386
|
-
assert.equal(afterStop.ran, false);
|
|
387
|
-
assert.equal(afterStop.reason, 'stopped');
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
test('scheduler: a run that straddles local midnight arms a catch-up tick', async () => {
|
|
391
|
-
const root = prepareRoot();
|
|
392
|
-
const clock = { value: new Date(2026, 10, 5, 23, 59, 59) };
|
|
393
|
-
const timers = fakeTimers();
|
|
394
|
-
let advanceOnRun = true;
|
|
395
|
-
const scheduler = createMaintenanceScheduler({
|
|
396
|
-
memoryRoot: root,
|
|
397
|
-
config: { runOnStart: false, consolidate: true, forget: true },
|
|
398
|
-
now: () => clock.value,
|
|
399
|
-
timers,
|
|
400
|
-
run: async (options) => {
|
|
401
|
-
if (advanceOnRun) {
|
|
402
|
-
advanceOnRun = false;
|
|
403
|
-
clock.value = new Date(2026, 10, 6, 0, 0, 1);
|
|
404
|
-
}
|
|
405
|
-
return runDailyMaintenance(options);
|
|
406
|
-
},
|
|
407
|
-
});
|
|
408
|
-
scheduler.start();
|
|
409
|
-
const result = await timers.fire(timers.next());
|
|
410
|
-
assert.equal(result.ran, true);
|
|
411
|
-
assert.equal(timers.count(), 1);
|
|
412
|
-
assert.ok(timers.delays()[0] <= 1000, 'catch-up tick should be short, not a full day');
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
test('corrupt schedule.json is tolerated and repaired with a backup', () => {
|
|
416
|
-
const root = prepareRoot();
|
|
417
|
-
put(root, '.longlive/bridge/schedule.json', '{not json');
|
|
418
|
-
const result = runDailyMaintenance({ memoryRoot: root, now: DAY1, config: {} });
|
|
419
|
-
|
|
420
|
-
assert.equal(result.ok, true);
|
|
421
|
-
assert.equal(result.ran, true);
|
|
422
|
-
assert.equal(result.corruptState, true);
|
|
423
|
-
const state = readSchedule(root);
|
|
424
|
-
assert.equal(state.lastConsolidateDate, '2026-11-05');
|
|
425
|
-
const backups = fs.readdirSync(path.join(root, '.longlive', 'backups'));
|
|
426
|
-
assert.ok(backups.some((name) => name.startsWith('schedule.')), 'corrupt state should be backed up');
|
|
427
|
-
});
|
|
428
|
-
|
|
429
|
-
test('config normalization: autoMaintenance is off by default and clamps hostile values', () => {
|
|
430
|
-
const off = normalizeFeatureConfig({});
|
|
431
|
-
assert.equal(off.autoMaintenance.enabled, false);
|
|
432
|
-
assert.equal(off.autoMaintenance.consolidate, true);
|
|
433
|
-
assert.equal(off.autoMaintenance.forget, true);
|
|
434
|
-
assert.equal(off.autoMaintenance.runOnStart, true);
|
|
435
|
-
assert.equal(off.autoMaintenance.startDelayMs, 500);
|
|
436
|
-
|
|
437
|
-
const on = normalizeFeatureConfig({
|
|
438
|
-
autoMaintenance: { enabled: true, consolidate: false, startDelayMs: -50 },
|
|
439
|
-
});
|
|
440
|
-
assert.deepEqual(on.autoMaintenance, {
|
|
441
|
-
enabled: true,
|
|
442
|
-
consolidate: false,
|
|
443
|
-
forget: true,
|
|
444
|
-
runOnStart: true,
|
|
445
|
-
startDelayMs: 0,
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
// Unknown/malformed blocks stay off; no throw.
|
|
449
|
-
const hostile = normalizeFeatureConfig({ autoMaintenance: 'yes' });
|
|
450
|
-
assert.equal(hostile.autoMaintenance.enabled, false);
|
|
451
|
-
});
|
|
452
|
-
|
|
453
|
-
test('readScheduleState reports missing/corrupt without throwing', () => {
|
|
454
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'longlive-maint-'));
|
|
455
|
-
const missing = readScheduleState(root);
|
|
456
|
-
assert.equal(missing.exists, false);
|
|
457
|
-
assert.equal(missing.corrupt, false);
|
|
458
|
-
assert.equal(missing.state.lastConsolidateDate, null);
|
|
459
|
-
|
|
460
|
-
put(root, '.longlive/bridge/schedule.json', '[]');
|
|
461
|
-
const corrupt = readScheduleState(root);
|
|
462
|
-
assert.equal(corrupt.exists, true);
|
|
463
|
-
assert.equal(corrupt.corrupt, true);
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
function fakeFeatureCtx() {
|
|
467
|
-
const effects = [];
|
|
468
|
-
const warnings = [];
|
|
469
|
-
const infos = [];
|
|
470
|
-
return {
|
|
471
|
-
effects,
|
|
472
|
-
warnings,
|
|
473
|
-
infos,
|
|
474
|
-
tools: { register() {} },
|
|
475
|
-
on() {
|
|
476
|
-
return () => {};
|
|
477
|
-
},
|
|
478
|
-
effect(register) {
|
|
479
|
-
effects.push(register);
|
|
480
|
-
return () => {};
|
|
481
|
-
},
|
|
482
|
-
logger() {
|
|
483
|
-
return {
|
|
484
|
-
warn: (message) => warnings.push(message),
|
|
485
|
-
info: (message) => infos.push(message),
|
|
486
|
-
};
|
|
487
|
-
},
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
test('features wiring: autoMaintenance starts one timer and registers a disposer', async () => {
|
|
492
|
-
const root = prepareRoot();
|
|
493
|
-
const clock = { value: new Date(2026, 10, 5, 10, 0, 0) };
|
|
494
|
-
const timers = fakeTimers();
|
|
495
|
-
const ctx = fakeFeatureCtx();
|
|
496
|
-
const registered = registerFeatures(ctx, {
|
|
497
|
-
autoMaintenance: { enabled: true, runOnStart: false },
|
|
498
|
-
now: () => clock.value,
|
|
499
|
-
timers,
|
|
500
|
-
}, { memoryRoot: root });
|
|
501
|
-
|
|
502
|
-
assert.ok(registered.enabled.includes('autoMaintenance'));
|
|
503
|
-
assert.equal(timers.count(), 1);
|
|
504
|
-
assert.equal(ctx.effects.length, 1);
|
|
505
|
-
const dispose = ctx.effects[0]();
|
|
506
|
-
assert.equal(typeof dispose, 'function');
|
|
507
|
-
dispose();
|
|
508
|
-
assert.equal(timers.count(), 0);
|
|
509
|
-
});
|
|
510
|
-
|
|
511
|
-
test('features wiring: autoMaintenance off means no timer and no schedule file', () => {
|
|
512
|
-
const root = prepareRoot();
|
|
513
|
-
const timers = fakeTimers();
|
|
514
|
-
const ctx = fakeFeatureCtx();
|
|
515
|
-
const registered = registerFeatures(ctx, { timers }, { memoryRoot: root });
|
|
516
|
-
|
|
517
|
-
assert.deepEqual(registered.enabled, []);
|
|
518
|
-
assert.equal(timers.count(), 0);
|
|
519
|
-
assert.equal(fs.existsSync(path.join(root, '.longlive', 'bridge', 'schedule.json')), false);
|
|
520
|
-
assert.deepEqual(ctx.effects, []);
|
|
521
|
-
assert.deepEqual(ctx.warnings, []);
|
|
522
|
-
assert.deepEqual(ctx.infos, []);
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
test('features wiring: both jobs off arms no timer and logs one warning', () => {
|
|
526
|
-
const root = prepareRoot();
|
|
527
|
-
const timers = fakeTimers();
|
|
528
|
-
const ctx = fakeFeatureCtx();
|
|
529
|
-
const registered = registerFeatures(ctx, {
|
|
530
|
-
autoMaintenance: { enabled: true, consolidate: false, forget: false },
|
|
531
|
-
timers,
|
|
532
|
-
}, { memoryRoot: root });
|
|
533
|
-
|
|
534
|
-
assert.deepEqual(registered.enabled, []);
|
|
535
|
-
assert.equal(timers.count(), 0);
|
|
536
|
-
assert.deepEqual(ctx.effects, []);
|
|
537
|
-
assert.equal(ctx.warnings.length, 1);
|
|
538
|
-
});
|